Skip to content

Commit a429c52

Browse files
authored
Rust support (#623)
1 parent 5c5d5f8 commit a429c52

27 files changed

Lines changed: 707 additions & 100 deletions

.eslintignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
/test/integration/hmr-dynamic/index.js
1313
/test/integration/wasm-async/index.js
1414
/test/integration/wasm-dynamic/index.js
15+
/test/integration/rust/index.js
16+
/test/integration/rust-deps/index.js
17+
/test/integration/rust-cargo/src/index.js
1518

1619
# Generated by the build
1720
lib

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ lib
1111
!test/**/node_modules
1212
.vscode/
1313
.idea/
14-
*.min.js
14+
*.min.js
15+
test/integration/**/target
16+
test/integration/**/Cargo.lock

.travis.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ node_js:
33
# TODO: Run Babel on tests so that async-await works in Node 6
44
# - '6'
55
- '8'
6-
cache: yarn
7-
script:
6+
cache:
7+
yarn: true
8+
cargo: true
9+
before_install:
10+
- curl https://sh.rustup.rs -sSf | sh -s -- -y
11+
- export PATH=/home/travis/.cargo/bin:$PATH
12+
script:
813
- yarn test-ci
914
- yarn lint
1015
sudo: false

appveyor.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ install:
99
# install modules
1010
- yarn install
1111

12+
# Install Rust and Cargo
13+
# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml)
14+
- curl -sSf -o rustup-init.exe https://win.rustup.rs
15+
- rustup-init.exe -y
16+
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
17+
- rustc -Vv
18+
- cargo -V
19+
1220
# Post-install test scripts.
1321
test_script:
1422
# Output useful info for debugging.
@@ -21,6 +29,7 @@ test_script:
2129

2230
cache:
2331
- "%LOCALAPPDATA%\\Yarn"
32+
- C:\Users\appveyor\.cargo
2433

2534
# Don't actually build.
2635
build: off

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"browser-resolve": "^1.11.2",
1818
"chalk": "^2.1.0",
1919
"chokidar": "^1.7.0",
20+
"command-exists": "^1.2.2",
2021
"commander": "^2.11.0",
2122
"cross-spawn": "^5.1.0",
2223
"cssnano": "^3.10.0",
@@ -40,6 +41,8 @@
4041
"sanitize-filename": "^1.6.1",
4142
"serve-static": "^1.12.4",
4243
"source-map": "0.6.1",
44+
"toml": "^2.3.3",
45+
"tomlify-j0.4": "^3.0.0",
4346
"uglify-es": "^3.2.1",
4447
"v8-compile-cache": "^1.1.0",
4548
"worker-farm": "^1.4.1",

src/Asset.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class Asset {
6060

6161
if (this.contents && this.mightHaveDependencies()) {
6262
await this.parseIfNeeded();
63-
this.collectDependencies();
63+
await this.collectDependencies();
6464
}
6565
}
6666

src/Parser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class Parser {
3131
this.registerExtension('scss', './assets/SASSAsset');
3232

3333
this.registerExtension('html', './assets/HTMLAsset');
34+
this.registerExtension('rs', './assets/RustAsset');
3435

3536
let extensions = options.extensions || {};
3637
for (let ext in extensions) {

src/WorkerFarm.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,18 @@ class WorkerFarm extends Farm {
6363
// While we're waiting, just run on the main thread.
6464
// This significantly speeds up startup time.
6565
if (this.started && this.warmWorkers >= this.activeChildren) {
66-
return this.remoteWorker.run(...args);
66+
return this.remoteWorker.run(...args, false);
6767
} else {
6868
// Workers have started, but are not warmed up yet.
6969
// Send the job to a remote worker in the background,
7070
// but use the result from the local worker - it will be faster.
7171
if (this.started) {
72-
this.remoteWorker.run(...args).then(() => {
72+
this.remoteWorker.run(...args, true).then(() => {
7373
this.warmWorkers++;
7474
});
7575
}
7676

77-
return this.localWorker.run(...args);
77+
return this.localWorker.run(...args, false);
7878
}
7979
}
8080

src/assets/RustAsset.js

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

src/packagers/RawPackager.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ class RawPackager extends Packager {
1818
);
1919
}
2020

21-
let contents =
22-
asset.generated[asset.type] || (await fs.readFile(asset.name));
21+
let contents = asset.generated[asset.type];
22+
if (!contents || (contents && contents.path)) {
23+
contents = await fs.readFile(contents ? contents.path : asset.name);
24+
}
25+
2326
await fs.writeFile(name, contents);
2427
}
2528

0 commit comments

Comments
 (0)