Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/wapython
Path: blob/main/core/wasi-js/src/fs.ts
1067 views
1
/*
2
Create a union filesystem as described by a FileSystemSpec[].
3
4
This code should not depend on anything that must run in node.js.
5
6
Note that this is entirely synchronous code, e.g., the unzip code,
7
and that's justified because our WASM interpreter will likely get
8
run in a different thread (a webworker) than the main thread, and
9
this code is needed to initialize it before anything else can happen.
10
*/
11
12
import unzip from "./unzip";
13
import {
14
Volume,
15
createFsFromVolume,
16
fs as memfs,
17
DirectoryJSON,
18
} from "@cowasm/memfs";
19
import { Union } from "@wapython/unionfs";
20
import { WASIFileSystem } from "./types";
21
22
// The native filesystem
23
interface NativeFs {
24
type: "native";
25
}
26
27
interface DevFs {
28
type: "dev";
29
}
30
31
interface ZipFsFile {
32
// has to be converted to ZipFs before passed in here.
33
type: "zipfile";
34
zipfile: string;
35
mountpoint: string;
36
async?: boolean; // if true, will load asynchronously in the background.
37
}
38
39
interface ZipFsUrl {
40
// has to be converted to ZipFs before passed in here.
41
type: "zipurl";
42
zipurl: string;
43
mountpoint: string;
44
async?: boolean; // if true, will load asynchronously in the background.
45
}
46
47
interface ZipFs {
48
type: "zip";
49
data: Buffer;
50
mountpoint: string;
51
}
52
53
interface ZipFsAsync {
54
type: "zip-async";
55
getData: () => Promise<Buffer>;
56
mountpoint: string;
57
}
58
59
interface MemFs {
60
type: "mem";
61
contents?: DirectoryJSON;
62
}
63
64
export type FileSystemSpec =
65
| NativeFs
66
| ZipFs
67
| ZipFsAsync
68
| ZipFsFile
69
| ZipFsUrl
70
| MemFs
71
| DevFs;
72
73
export function createFileSystem(
74
specs: FileSystemSpec[],
75
nativeFs?: WASIFileSystem
76
): WASIFileSystem {
77
if (specs.length == 0) {
78
return memFs(); // empty memfs
79
}
80
if (specs.length == 1) {
81
// don't use unionfs:
82
return specToFs(specs[0], nativeFs) ?? memFs();
83
}
84
const ufs = new Union();
85
const v: Function[] = [];
86
for (const spec of specs) {
87
const fs = specToFs(spec, nativeFs);
88
if (fs != null) {
89
// e.g., native bindings may be null.
90
ufs.use(fs);
91
if (fs.waitUntilLoaded != null) {
92
v.push(fs.waitUntilLoaded.bind(fs));
93
}
94
}
95
}
96
const waitUntilLoaded = async () => {
97
for (const wait of v) {
98
await wait();
99
}
100
};
101
return { ...ufs, constants: memfs.constants, waitUntilLoaded };
102
}
103
104
function specToFs(
105
spec: FileSystemSpec,
106
nativeFs?: WASIFileSystem
107
): WASIFileSystem | undefined {
108
// All these "as any" are because really nothing quite implements FileSystem yet!
109
// See https://github.com/streamich/memfs/issues/735
110
if (spec.type == "zip") {
111
return zipFs(spec.data, spec.mountpoint) as any;
112
} else if (spec.type == "zip-async") {
113
return zipFsAsync(spec.getData, spec.mountpoint) as any;
114
} else if (spec.type == "zipfile") {
115
throw Error(`you must convert zipfile -- read ${spec.zipfile} into memory`);
116
} else if (spec.type == "zipurl") {
117
throw Error(`you must convert zipurl -- read ${spec.zipurl} into memory`);
118
} else if (spec.type == "native") {
119
// native = whatever is in bindings.
120
return nativeFs == null ? nativeFs : mapFlags(nativeFs);
121
} else if (spec.type == "mem") {
122
return memFs(spec.contents) as any;
123
} else if (spec.type == "dev") {
124
return devFs() as any;
125
}
126
throw Error(`unknown spec type - ${JSON.stringify(spec)}`);
127
}
128
129
// this is generic and would work in a browser:
130
function devFs(): WASIFileSystem {
131
const vol = Volume.fromJSON({
132
"/dev/stdin": "",
133
"/dev/stdout": "",
134
"/dev/stderr": "",
135
});
136
vol.releasedFds = [0, 1, 2];
137
const fdErr = vol.openSync("/dev/stderr", "w");
138
const fdOut = vol.openSync("/dev/stdout", "w");
139
const fdIn = vol.openSync("/dev/stdin", "r");
140
if (fdErr != 2) throw Error(`invalid handle for stderr: ${fdErr}`);
141
if (fdOut != 1) throw Error(`invalid handle for stdout: ${fdOut}`);
142
if (fdIn != 0) throw Error(`invalid handle for stdin: ${fdIn}`);
143
return createFsFromVolume(vol) as unknown as WASIFileSystem;
144
}
145
146
function zipFs(data: Buffer, directory: string = "/"): WASIFileSystem {
147
const fs = createFsFromVolume(new Volume()) as any;
148
fs.mkdirSync(directory, { recursive: true });
149
unzip({ data, fs, directory });
150
return fs;
151
}
152
153
function zipFsAsync(
154
getData: () => Promise<Buffer>,
155
directory: string = "/"
156
): WASIFileSystem {
157
const fs = createFsFromVolume(new Volume()) as any;
158
const load = async () => {
159
let data;
160
try {
161
data = await getData();
162
} catch (err) {
163
console.warn(
164
`FAILED to load async filesystem for '${directory}' - ${err}`
165
);
166
throw err;
167
}
168
// NOTE: there is an async version of this, but it runs in another
169
// webworker and costs significant overhead, so not worth it.
170
unzip({ data, fs, directory });
171
};
172
const loadingPromise = load();
173
fs.waitUntilLoaded = () => loadingPromise;
174
return fs;
175
}
176
177
function memFs(contents?: DirectoryJSON): WASIFileSystem {
178
const vol = contents != null ? Volume.fromJSON(contents) : new Volume();
179
return createFsFromVolume(vol) as unknown as WASIFileSystem;
180
}
181
182
function mapFlags(nativeFs: WASIFileSystem): WASIFileSystem {
183
function translate(flags: number): number {
184
// We have to translate the flags from WASM/memfs/musl to native for this operating system.
185
// E.g., on MacOS many flags are completely different. See big comment below.
186
let nativeFlags = 0;
187
for (const flag in memfs.constants) {
188
// only flags starting with O_ are relevant for the open syscall.
189
if (flag.startsWith("O_") && flags & memfs.constants[flag]) {
190
nativeFlags |= nativeFs.constants[flag];
191
}
192
}
193
return nativeFlags;
194
}
195
// "any" because there's something weird involving a __promises__ namespace that I don't understand.
196
const open: any = async (path, flags, mode?) => {
197
return await nativeFs.open(path, translate(flags), mode);
198
};
199
const openSync = (path, flags, mode?) => {
200
return nativeFs.openSync(path, translate(flags), mode);
201
};
202
const promises = {
203
...nativeFs.promises,
204
open: async (path, flags, mode?) => {
205
return await nativeFs.promises.open(path, flags, mode);
206
},
207
};
208
return {
209
...{ ...nativeFs, promises },
210
open,
211
openSync,
212
constants: memfs.constants, // critical to ALWAYS use memfs constants for any filesystem.
213
};
214
}
215
216
/*
217
Comment about flags:
218
219
A major subtle issue I hit is that unionfs combines filesystems, and
220
each filesystem can define fs.constants differently! In particular,
221
memfs always hardcodes constants.O_EXCL to be 128. However, on
222
macos native filesystem it is 2048, whereas on Linux native filesystem
223
it is also 128. We combine memfs and native for running python-wasm
224
under nodejs, since we want to use our Python install (that is in
225
dist/python/python.zip and mounted using memfs) along with full access
226
to the native filesystem.
227
228
I think the only good solution to this is the following:
229
- if native isn't part of the unionfs, nothing to do (since we only currently use native and memfs).
230
- fs.constants should be memfs's constants since I think they match with what WebAssembly libc (via musl)
231
provides.
232
- in the node api, the ONLY functions that take numeric flags are open and openSync. That's convenient!
233
- somehow figure out which filesystem (native or memfs for now) that a given open will go to, and
234
convert the flags if going to memfs.
235
236
Probably the easiest way to accomplish all of the above is just use a proxy around native fs's
237
open* function.
238
*/
239
240