1 | const path = require('path');
|
2 | const commandExists = require('command-exists');
|
3 | const childProcess = require('child_process');
|
4 | const {promisify} = require('@parcel/utils');
|
5 | const exec = promisify(childProcess.execFile);
|
6 | const toml = require('@iarna/toml');
|
7 | const fs = require('@parcel/fs');
|
8 | const Asset = require('../Asset');
|
9 | const config = require('../utils/config');
|
10 | const pipeSpawn = require('../utils/pipeSpawn');
|
11 | const md5 = require('../utils/md5');
|
12 |
|
13 | const RUST_TARGET = 'wasm32-unknown-unknown';
|
14 | const MAIN_FILES = ['src/lib.rs', 'src/main.rs'];
|
15 |
|
16 |
|
17 | let rustInstalled = false;
|
18 |
|
19 | class RustAsset extends Asset {
|
20 | constructor(name, options) {
|
21 | super(name, options);
|
22 | this.type = 'wasm';
|
23 | }
|
24 |
|
25 | process() {
|
26 |
|
27 |
|
28 |
|
29 | if (this.options.isWarmUp) {
|
30 | return;
|
31 | }
|
32 |
|
33 | return super.process();
|
34 | }
|
35 |
|
36 | async parse() {
|
37 |
|
38 | await this.installRust();
|
39 |
|
40 |
|
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 |
|
58 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
126 | let args = ['+nightly', 'build', '--target', RUST_TARGET, '--release'];
|
127 | await exec('cargo', args, {cwd: cargoDir});
|
128 |
|
129 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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,
|
190 | mtime: Date.now()
|
191 | }
|
192 | };
|
193 | }
|
194 | }
|
195 |
|
196 | module.exports = RustAsset;
|