UNPKG

5.39 kBJavaScriptView Raw
1const path = require('path');
2const commandExists = require('command-exists');
3const childProcess = require('child_process');
4const {promisify} = require('@parcel/utils');
5const exec = promisify(childProcess.execFile);
6const toml = require('@iarna/toml');
7const fs = require('@parcel/fs');
8const Asset = require('../Asset');
9const config = require('../utils/config');
10const pipeSpawn = require('../utils/pipeSpawn');
11const md5 = require('../utils/md5');
12
13const RUST_TARGET = 'wasm32-unknown-unknown';
14const MAIN_FILES = ['src/lib.rs', 'src/main.rs'];
15
16// Track installation status so we don't need to check more than once
17let rustInstalled = false;
18
19class RustAsset extends Asset {
20 constructor(name, options) {
21 super(name, options);
22 this.type = 'wasm';
23 }
24
25 process() {
26 // We don't want to process this asset if the worker is in a warm up phase
27 // since the asset will also be processed by the main process, which
28 // may cause errors since rust writes to the filesystem.
29 if (this.options.isWarmUp) {
30 return;
31 }
32
33 return super.process();
34 }
35
36 async parse() {
37 // Install rust toolchain and target if needed
38 await this.installRust();
39
40 // See if there is a Cargo config in the project
41 let cargoConfig = await this.getConfig(['Cargo.toml']);
42 let cargoDir;
43 let isMainFile = false;
44
45 if (cargoConfig) {
46 const mainFiles = MAIN_FILES.slice();
47 if (cargoConfig.lib && cargoConfig.lib.path) {
48 mainFiles.push(cargoConfig.lib.path);
49 }
50
51 cargoDir = path.dirname(await config.resolve(this.name, ['Cargo.toml']));
52 isMainFile = mainFiles.some(
53 file => path.join(cargoDir, file) === this.name
54 );
55 }
56
57 // If this is the main file of a Cargo build, use the cargo command to compile.
58 // Otherwise, use rustc directly.
59 if (isMainFile) {
60 await this.cargoBuild(cargoConfig, cargoDir);
61 } else {
62 await this.rustcBuild();
63 }
64 }
65
66 async installRust() {
67 if (rustInstalled) {
68 return;
69 }
70
71 // Check for rustup
72 try {
73 await commandExists('rustup');
74 } catch (e) {
75 throw new Error(
76 "Rust isn't installed. Visit https://www.rustup.rs/ for more info"
77 );
78 }
79
80 // Ensure nightly toolchain is installed
81 let [stdout] = await exec('rustup', ['show']);
82 if (!stdout.includes('nightly')) {
83 await pipeSpawn('rustup', ['update']);
84 await pipeSpawn('rustup', ['toolchain', 'install', 'nightly']);
85 }
86
87 // Ensure wasm target is installed
88 [stdout] = await exec('rustup', [
89 'target',
90 'list',
91 '--toolchain',
92 'nightly'
93 ]);
94 if (!stdout.includes(RUST_TARGET + ' (installed)')) {
95 await pipeSpawn('rustup', [
96 'target',
97 'add',
98 RUST_TARGET,
99 '--toolchain',
100 'nightly'
101 ]);
102 }
103
104 rustInstalled = true;
105 }
106
107 async cargoBuild(cargoConfig, cargoDir) {
108 // Ensure the cargo config has cdylib as the crate-type
109 if (!cargoConfig.lib) {
110 cargoConfig.lib = {};
111 }
112
113 if (!Array.isArray(cargoConfig.lib['crate-type'])) {
114 cargoConfig.lib['crate-type'] = [];
115 }
116
117 if (!cargoConfig.lib['crate-type'].includes('cdylib')) {
118 cargoConfig.lib['crate-type'].push('cdylib');
119 await fs.writeFile(
120 path.join(cargoDir, 'Cargo.toml'),
121 toml.stringify(cargoConfig)
122 );
123 }
124
125 // Run cargo
126 let args = ['+nightly', 'build', '--target', RUST_TARGET, '--release'];
127 await exec('cargo', args, {cwd: cargoDir});
128
129 // Get output file paths
130 let [stdout] = await exec('cargo', ['metadata', '--format-version', '1'], {
131 cwd: cargoDir
132 });
133 const cargoMetadata = JSON.parse(stdout);
134 const cargoTargetDir = cargoMetadata.target_directory;
135 let outDir = path.join(cargoTargetDir, RUST_TARGET, 'release');
136
137 // Rust converts '-' to '_' when outputting files.
138 let rustName = cargoConfig.package.name.replace(/-/g, '_');
139 this.wasmPath = path.join(outDir, rustName + '.wasm');
140 this.depsPath = path.join(outDir, rustName + '.d');
141 }
142
143 async rustcBuild() {
144 // Get output filename
145 await fs.mkdirp(this.options.cacheDir);
146 let name = md5(this.name);
147 this.wasmPath = path.join(this.options.cacheDir, name + '.wasm');
148
149 // Run rustc to compile the code
150 const args = [
151 '+nightly',
152 '--target',
153 RUST_TARGET,
154 '-O',
155 '--crate-type=cdylib',
156 this.name,
157 '-o',
158 this.wasmPath
159 ];
160
161 await exec('rustc', args);
162
163 // Run again to collect dependencies
164 this.depsPath = path.join(this.options.cacheDir, name + '.d');
165 await exec('rustc', [this.name, '--emit=dep-info', '-o', this.depsPath]);
166 }
167
168 async collectDependencies() {
169 // Read deps file
170 let contents = await fs.readFile(this.depsPath, 'utf8');
171 let dir = path.dirname(this.name);
172
173 let deps = contents
174 .split('\n')
175 .filter(Boolean)
176 .slice(1);
177
178 for (let dep of deps) {
179 dep = path.resolve(dir, dep.slice(0, dep.indexOf(': ')));
180 if (dep !== this.name) {
181 this.addDependency(dep, {includedInParent: true});
182 }
183 }
184 }
185
186 async generate() {
187 return {
188 wasm: {
189 path: this.wasmPath, // pass output path to RawPackager
190 mtime: Date.now() // force re-bundling since otherwise the hash would never change
191 }
192 };
193 }
194}
195
196module.exports = RustAsset;