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