diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 309b3d5af4..872b5623f8 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Version: 9 +# Version: 10 # A script that installs the dependencies needed to build and test Bun. # This should work on macOS and Linux with a POSIX shell. @@ -919,10 +919,7 @@ install_llvm() { "llvm$(llvm_version)" \ "clang$(llvm_version)" \ "scudo-malloc" \ - --repository "http://dl-cdn.alpinelinux.org/alpine/edge/main" - install_packages \ - "lld$(llvm_version)" \ - --repository "http://dl-cdn.alpinelinux.org/alpine/edge/community" + "lld$(llvm_version)" ;; esac } @@ -1132,6 +1129,35 @@ install_tailscale() { esac } +install_fuse_python() { + # only linux needs this + case "$pm" in + apk) + # Build and install from source (https://github.com/libfuse/python-fuse/blob/master/INSTALL) + install_packages \ + python3-dev \ + fuse-dev \ + pkgconf \ + py3-setuptools + python_fuse_version="1.0.9" + python_fuse_tarball=$(download_file "https://github.com/libfuse/python-fuse/archive/refs/tags/v$python_fuse_version.tar.gz") + python_fuse_tmpdir="$(dirname "$python_fuse_tarball")" + execute tar -xzf "$python_fuse_tarball" -C "$python_fuse_tmpdir" + execute sh -c "cd '$python_fuse_tmpdir/python-fuse-$python_fuse_version' && python setup.py build" + execute_sudo sh -c "cd '$python_fuse_tmpdir/python-fuse-$python_fuse_version' && python setup.py install" + + # For Alpine we also need to make sure the kernel module is automatically loaded + execute_sudo sh -c "echo fuse >> /etc/modules-load.d/fuse.conf" + + # Check that it was actually installed + execute python -c 'import fuse' + ;; + apt | dnf | yum) + install_packages python3-fuse + ;; + esac +} + create_buildkite_user() { if ! [ "$ci" = "1" ] || ! [ "$os" = "linux" ]; then return @@ -1323,6 +1349,7 @@ main() { install_common_software install_build_essentials install_chromium + install_fuse_python clean_system } diff --git a/src/fs.zig b/src/fs.zig index 1593be55d1..7d9ba839ca 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -147,12 +147,26 @@ pub const FileSystem = struct { pub fn addEntry(dir: *DirEntry, prev_map: ?*EntryMap, entry: *const bun.DirIterator.IteratorResult, allocator: std.mem.Allocator, comptime Iterator: type, iterator: Iterator) !void { const name_slice = entry.name.slice(); - const _kind: Entry.Kind = switch (entry.kind) { + const found_kind: ?Entry.Kind = switch (entry.kind) { .directory => .dir, - // This might be wrong! - .sym_link => .file, .file => .file, - else => return, + + // For a symlink, we will need to stat the target later + .sym_link, + // Some filesystems return `.unknown` from getdents() no matter the actual kind of the file + // (often because it would be slow to look up the kind). If we get this, then code that + // needs the kind will have to find it out later by calling stat(). + .unknown, + => null, + + .block_device, + .character_device, + .named_pipe, + .unix_domain_socket, + .whiteout, + .door, + .event_port, + => return, }; const stored = try brk: { @@ -166,10 +180,14 @@ pub const FileSystem = struct { defer existing.mutex.unlock(); existing.dir = dir.dir; - existing.need_stat = existing.need_stat or existing.cache.kind != _kind; + existing.need_stat = existing.need_stat or + found_kind == null or + existing.cache.kind != found_kind; // TODO: is this right? - if (existing.cache.kind != _kind) { - existing.cache.kind = _kind; + if (existing.cache.kind != found_kind) { + // if found_kind is null, we have set need_stat above, so we + // store an arbitrary kind + existing.cache.kind = found_kind orelse .file; existing.cache.symlink = PathString.empty; } @@ -198,10 +216,12 @@ pub const FileSystem = struct { // Call "stat" lazily for performance. The "@material-ui/icons" package // contains a directory with over 11,000 entries in it and running "stat" // for each entry was a big performance issue for that package. - .need_stat = entry.kind == .sym_link, + .need_stat = found_kind == null, .cache = .{ .symlink = PathString.empty, - .kind = _kind, + // if found_kind is null, we have set need_stat above, so we + // store an arbitrary kind + .kind = found_kind orelse .file, }, }); }; @@ -215,7 +235,7 @@ pub const FileSystem = struct { } if (comptime FeatureFlags.verbose_fs) { - if (_kind == .dir) { + if (found_kind == .dir) { Output.prettyln(" + {s}/", .{stored_name}); } else { Output.prettyln(" + {s}", .{stored_name}); @@ -1844,7 +1864,7 @@ pub const Path = struct { if ((FileSystem.FilenameStore.instance.exists(this.text) or FileSystem.DirnameStore.instance.exists(this.text)) and (FileSystem.FilenameStore.instance.exists(this.pretty) or - FileSystem.DirnameStore.instance.exists(this.pretty))) + FileSystem.DirnameStore.instance.exists(this.pretty))) { return this.*; } @@ -1950,13 +1970,6 @@ pub const Path = struct { }; } - pub fn isBefore(a: *Path, b: Path) bool { - return a.namespace > b.namespace || - (a.namespace == b.namespace and (a.text < b.text || - (a.text == b.text and (a.flags < b.flags || - (a.flags == b.flags))))); - } - pub fn isNodeModule(this: *const Path) bool { return strings.lastIndexOf(this.name.dir, std.fs.path.sep_str ++ "node_modules" ++ std.fs.path.sep_str) != null; } diff --git a/test/cli/run/fuse-fs.py b/test/cli/run/fuse-fs.py new file mode 100644 index 0000000000..61cc7339e2 --- /dev/null +++ b/test/cli/run/fuse-fs.py @@ -0,0 +1,69 @@ +# Basic filesystem with FUSE +# Used to ensure bun can run files mounted on FUSE +# The filesystem will appear to have `main.js` containing: +# console.log("hello world"); +# and `main-symlink.js` as a symlink to `main.js`. +import fuse +import errno, stat, os + +fuse.fuse_python_api = (0, 2) + +script = b'console.log("hello world");\n' + + +class TestingFs(fuse.Fuse): + def getattr(self, path): + st = fuse.Stat() + if path == "/": + st.st_mode = stat.S_IFDIR | 0o755 + st.st_nlink = 2 + elif path == "/main.js": + st.st_mode = stat.S_IFREG | 0o644 + st.st_nlink = 1 + st.st_size = len(script) + elif path == "/main-symlink.js": + st.st_mode = stat.S_IFLNK | 0o644 + st.st_nlink = 1 + st.st_size = len("main.js") + else: + return -errno.ENOENT + return st + + def readdir(self, path, offset): + for r in ".", "..", "main.js", "main-symlink.js": + yield fuse.Direntry(r) + + def open(self, path, flags): + if path != "/main.js" and path != "/main-symlink.js": + return -errno.ENOENT + mask = os.O_RDONLY | os.O_WRONLY | os.O_RDWR + if (flags & mask) != os.O_RDONLY: + return -errno.EACCES + + def read(self, path, size, offset): + if path != "/main.js": + return -errno.ENOENT + if offset < len(script): + if offset + size > len(script): + size = len(script) - offset + return script[offset : offset + size] + return b"" + + def readlink(self, path): + if path != "/main-symlink.js": + return -errno.ENOENT + return "main.js" + + +def main(): + server = TestingFs( + version=fuse.__version__, + usage="\nSmall filesystem made for testing Bun's ability to run files off of FUSE", + dash_s_do="setsingle", + ) + server.parse(errex=1) + server.main() + + +if __name__ == "__main__": + main() diff --git a/test/cli/run/run-file-on-fuse.test.ts b/test/cli/run/run-file-on-fuse.test.ts new file mode 100644 index 0000000000..69545a34f1 --- /dev/null +++ b/test/cli/run/run-file-on-fuse.test.ts @@ -0,0 +1,61 @@ +import { bunEnv, bunExe, isLinux, tmpdirSync } from "harness"; +import { ReadableSubprocess, spawn } from "bun"; +import { describe, expect, test, beforeAll } from "bun:test"; +import { join } from "node:path"; +import fs from "node:fs"; + +describe.skipIf(!isLinux)("running files on a FUSE mount", () => { + async function doTest(pathOnMount: string): Promise { + const mountpoint = tmpdirSync(); + + let pythonProcess: ReadableSubprocess | undefined = undefined; + try { + // setup FUSE filesystem + pythonProcess = spawn({ + cmd: ["python3", "fuse-fs.py", "-f", mountpoint], + cwd: __dirname, + stdout: "pipe", + stderr: "pipe", + }); + + // wait for it to work + let tries = 0; + while (!fs.existsSync(join(mountpoint, pathOnMount)) && tries < 250) { + tries++; + await Bun.sleep(5); + } + expect(fs.existsSync(join(mountpoint, pathOnMount))).toBeTrue(); + + // run bun + const bun = spawn({ + cmd: [bunExe(), join(mountpoint, pathOnMount)], + cwd: __dirname, + stdout: "pipe", + env: bunEnv, + }); + await Promise.race([bun.exited, Bun.sleep(1000)]); + expect(bun.exitCode).toBe(0); + expect(await new Response(bun.stdout).text()).toBe("hello world\n"); + } finally { + if (pythonProcess) { + try { + // run umount + const umount = spawn({ cmd: ["fusermount", "-u", mountpoint] }); + await umount.exited; + // wait for graceful exit + await Promise.race([pythonProcess.exited, Bun.sleep(1000)]); + expect(pythonProcess.exitCode).toBe(0); + } catch (e) { + pythonProcess.kill("SIGKILL"); + console.error("python process errored:", await new Response(pythonProcess.stderr).text()); + throw e; + } + } + } + } + + // set a long timeout so it is more likely doTest can clean up the filesystem mount itself + // rather than getting interrupted by timeout + test("regular file", () => doTest(join("main.js")), 10000); + test("symlink", () => doTest(join("main-symlink.js")), 10000); +});