Files
bun.sh/src/shell/builtin/touch.zig
pfg 05d0475c6c Update to zig 0.15.2 (#24204)
Fixes ENG-21287

Build times, from `bun run build && echo '//' >> src/main.zig && time
bun run build`

|Platform|0.14.1|0.15.2|Speedup|
|-|-|-|-|
|macos debug asan|126.90s|106.27s|1.19x|
|macos debug noasan|60.62s|50.85s|1.19x|
|linux debug asan|292.77s|241.45s|1.21x|
|linux debug noasan|146.58s|130.94s|1.12x|
|linux debug use_llvm=false|n/a|78.27s|1.87x|
|windows debug asan|177.13s|142.55s|1.24x|

Runtime performance:

- next build memory usage may have gone up by 5%. Otherwise seems the
same. Some code with writers may have gotten slower, especially one
instance of a counting writer and a few instances of unbuffered writers
that now have vtable overhead.
- File size reduced by 800kb (from 100.2mb to 99.4mb)

Improvements:

- `@export` hack is no longer needed for watch
- native x86_64 backend for linux builds faster. to use it, set use_llvm
false and no_link_obj false. also set `ASAN_OPTIONS=detect_leaks=0`
otherwise it will spam the output with tens of thousands of lines of
debug info errors. may need to use the zig lldb fork for debugging.
- zig test-obj, which we will be able to use for zig unit tests

Still an issue:

- false 'dependency loop' errors remain in watch mode
- watch mode crashes observed

Follow-up:

- [ ] search `comptime Writer: type` and `comptime W: type` and remove
- [ ] remove format_mode in our zig fork
- [ ] remove deprecated.zig autoFormatLabelFallback
- [ ] remove deprecated.zig autoFormatLabel
- [ ] remove deprecated.BufferedWriter and BufferedReader
- [ ] remove override_no_export_cpp_apis as it is no longer needed
- [ ] css Parser(W) -> Parser, and remove all the comptime writer: type
params
- [ ] remove deprecated writer fully

Files that add lines:

```
649     src/deprecated.zig
167     scripts/pack-codegen-for-zig-team.ts
54      scripts/cleartrace-impl.js
46      scripts/cleartrace.ts
43      src/windows.zig
18      src/fs.zig
17      src/bun.js/ConsoleObject.zig
16      src/output.zig
12      src/bun.js/test/debug.zig
12      src/bun.js/node/node_fs.zig
8       src/env_loader.zig
7       src/css/printer.zig
7       src/cli/init_command.zig
7       src/bun.js/node.zig
6       src/string/escapeRegExp.zig
6       src/install/PnpmMatcher.zig
5       src/bun.js/webcore/Blob.zig
4       src/crash_handler.zig
4       src/bun.zig
3       src/install/lockfile/bun.lock.zig
3       src/cli/update_interactive_command.zig
3       src/cli/pack_command.zig
3       build.zig
2       src/Progress.zig
2       src/install/lockfile/lockfile_json_stringify_for_debugging.zig
2       src/css/small_list.zig
2       src/bun.js/webcore/prompt.zig
1       test/internal/ban-words.test.ts
1       test/internal/ban-limits.json
1       src/watcher/WatcherTrace.zig
1       src/transpiler.zig
1       src/shell/builtin/cp.zig
1       src/js_printer.zig
1       src/io/PipeReader.zig
1       src/install/bin.zig
1       src/css/selectors/selector.zig
1       src/cli/run_command.zig
1       src/bun.js/RuntimeTranspilerStore.zig
1       src/bun.js/bindings/JSRef.zig
1       src/bake/DevServer.zig
```

Files that remove lines:

```
-1      src/test/recover.zig
-1      src/sql/postgres/SocketMonitor.zig
-1      src/sql/mysql/MySQLRequestQueue.zig
-1      src/sourcemap/CodeCoverage.zig
-1      src/css/values/color_js.zig
-1      src/compile_target.zig
-1      src/bundler/linker_context/convertStmtsForChunk.zig
-1      src/bundler/bundle_v2.zig
-1      src/bun.js/webcore/blob/read_file.zig
-1      src/ast/base.zig
-2      src/sql/postgres/protocol/ArrayList.zig
-2      src/shell/builtin/mkdir.zig
-2      src/install/PackageManager/patchPackage.zig
-2      src/install/PackageManager/PackageManagerDirectories.zig
-2      src/fmt.zig
-2      src/css/declaration.zig
-2      src/css/css_parser.zig
-2      src/collections/baby_list.zig
-2      src/bun.js/bindings/ZigStackFrame.zig
-2      src/ast/E.zig
-3      src/StandaloneModuleGraph.zig
-3      src/deps/picohttp.zig
-3      src/deps/libuv.zig
-3      src/btjs.zig
-4      src/threading/Futex.zig
-4      src/shell/builtin/touch.zig
-4      src/meta.zig
-4      src/install/lockfile.zig
-4      src/css/selectors/parser.zig
-5      src/shell/interpreter.zig
-5      src/css/error.zig
-5      src/bun.js/web_worker.zig
-5      src/bun.js.zig
-6      src/cli/test_command.zig
-6      src/bun.js/VirtualMachine.zig
-6      src/bun.js/uuid.zig
-6      src/bun.js/bindings/JSValue.zig
-9      src/bun.js/test/pretty_format.zig
-9      src/bun.js/api/BunObject.zig
-14     src/install/install_binding.zig
-14     src/fd.zig
-14     src/bun.js/node/path.zig
-14     scripts/pack-codegen-for-zig-team.sh
-17     src/bun.js/test/diff_format.zig
```

`git diff --numstat origin/main...HEAD | awk '{ print ($1-$2)"\t"$3 }' |
sort -rn`

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
Co-authored-by: Meghan Denny <meghan@bun.com>
Co-authored-by: tayor.fish <contact@taylor.fish>
2025-11-10 14:38:26 -08:00

413 lines
13 KiB
Zig

const Touch = @This();
opts: Opts = .{},
state: union(enum) {
idle,
exec: struct {
started: bool = false,
tasks_count: usize = 0,
tasks_done: usize = 0,
output_done: usize = 0,
output_waiting: usize = 0,
started_output_queue: bool = false,
args: []const [*:0]const u8,
err: ?jsc.SystemError = null,
},
waiting_write_err,
done,
} = .idle,
pub fn format(this: *const Touch, writer: *std.Io.Writer) !void {
try writer.print("Touch(0x{x}, state={s})", .{ @intFromPtr(this), @tagName(this.state) });
}
pub fn deinit(this: *Touch) void {
log("{f} deinit", .{this});
}
pub fn start(this: *Touch) Yield {
const filepath_args = switch (this.opts.parse(this.bltn().argsSlice())) {
.ok => |filepath_args| filepath_args,
.err => |e| {
const buf = switch (e) {
.illegal_option => |opt_str| this.bltn().fmtErrorArena(.touch, "illegal option -- {s}\n", .{opt_str}),
.show_usage => Builtin.Kind.touch.usageString(),
.unsupported => |unsupported| this.bltn().fmtErrorArena(.touch, "unsupported option, please open a GitHub issue -- {s}\n", .{unsupported}),
};
return this.writeFailingError(buf, 1);
},
} orelse {
return this.writeFailingError(Builtin.Kind.touch.usageString(), 1);
};
this.state = .{
.exec = .{
.args = filepath_args,
},
};
return this.next();
}
pub fn next(this: *Touch) Yield {
switch (this.state) {
.idle => @panic("Invalid state"),
.exec => {
var exec = &this.state.exec;
if (exec.started) {
if (this.state.exec.tasks_done >= this.state.exec.tasks_count and this.state.exec.output_done >= this.state.exec.output_waiting) {
const exit_code: ExitCode = if (this.state.exec.err != null) 1 else 0;
this.state = .done;
return this.bltn().done(exit_code);
}
return .suspended;
}
exec.started = true;
exec.tasks_count = exec.args.len;
for (exec.args) |dir_to_mk_| {
const dir_to_mk = dir_to_mk_[0..std.mem.len(dir_to_mk_) :0];
var task = ShellTouchTask.create(this, this.opts, dir_to_mk, this.bltn().parentCmd().base.shell.cwdZ());
task.schedule();
}
return .suspended;
},
.waiting_write_err => return .failed,
.done => return this.bltn().done(0),
}
}
pub fn onIOWriterChunk(this: *Touch, _: usize, e: ?jsc.SystemError) Yield {
if (this.state == .waiting_write_err) {
return this.bltn().done(1);
}
if (e) |err| err.deref();
return this.next();
}
pub fn writeFailingError(this: *Touch, buf: []const u8, exit_code: ExitCode) Yield {
if (this.bltn().stderr.needsIO()) |safeguard| {
this.state = .waiting_write_err;
return this.bltn().stderr.enqueue(this, buf, safeguard);
}
_ = this.bltn().writeNoIO(.stderr, buf);
return this.bltn().done(exit_code);
}
pub fn onShellTouchTaskDone(this: *Touch, task: *ShellTouchTask) void {
log("{f} onShellTouchTaskDone {f} tasks_done={d} tasks_count={d}", .{ this, task, this.state.exec.tasks_done, this.state.exec.tasks_count });
defer bun.default_allocator.destroy(task);
this.state.exec.tasks_done += 1;
const err = task.err;
if (err) |e| {
const output_task: *ShellTouchOutputTask = bun.new(ShellTouchOutputTask, .{
.parent = this,
.output = .{ .arrlist = .{} },
.state = .waiting_write_err,
});
const error_string = this.bltn().taskErrorToString(.touch, e);
this.state.exec.err = e;
output_task.start(error_string).run();
return;
}
this.next().run();
}
pub const ShellTouchOutputTask = OutputTask(Touch, .{
.writeErr = ShellTouchOutputTaskVTable.writeErr,
.onWriteErr = ShellTouchOutputTaskVTable.onWriteErr,
.writeOut = ShellTouchOutputTaskVTable.writeOut,
.onWriteOut = ShellTouchOutputTaskVTable.onWriteOut,
.onDone = ShellTouchOutputTaskVTable.onDone,
});
const ShellTouchOutputTaskVTable = struct {
pub fn writeErr(this: *Touch, childptr: anytype, errbuf: []const u8) ?Yield {
if (this.bltn().stderr.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
}
_ = this.bltn().writeNoIO(.stderr, errbuf);
return null;
}
pub fn onWriteErr(this: *Touch) void {
this.state.exec.output_done += 1;
}
pub fn writeOut(this: *Touch, childptr: anytype, output: *OutputSrc) ?Yield {
if (this.bltn().stdout.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
const slice = output.slice();
log("THE SLICE: {d} {s}", .{ slice.len, slice });
return this.bltn().stdout.enqueue(childptr, slice, safeguard);
}
_ = this.bltn().writeNoIO(.stdout, output.slice());
return null;
}
pub fn onWriteOut(this: *Touch) void {
this.state.exec.output_done += 1;
}
pub fn onDone(this: *Touch) Yield {
return this.next();
}
};
pub const ShellTouchTask = struct {
touch: *Touch,
opts: Opts,
filepath: [:0]const u8,
cwd_path: [:0]const u8,
err: ?jsc.SystemError = null,
task: jsc.WorkPoolTask = .{ .callback = &runFromThreadPool },
event_loop: jsc.EventLoopHandle,
concurrent_task: jsc.EventLoopTask,
pub fn format(this: *const ShellTouchTask, writer: *std.Io.Writer) !void {
try writer.print("ShellTouchTask(0x{x}, filepath={s})", .{ @intFromPtr(this), this.filepath });
}
pub fn deinit(this: *ShellTouchTask) void {
if (this.err) |*e| {
e.deref();
}
bun.default_allocator.destroy(this);
}
pub fn create(touch: *Touch, opts: Opts, filepath: [:0]const u8, cwd_path: [:0]const u8) *ShellTouchTask {
const task = bun.handleOom(bun.default_allocator.create(ShellTouchTask));
task.* = ShellTouchTask{
.touch = touch,
.opts = opts,
.cwd_path = cwd_path,
.filepath = filepath,
.event_loop = touch.bltn().eventLoop(),
.concurrent_task = jsc.EventLoopTask.fromEventLoop(touch.bltn().eventLoop()),
};
return task;
}
pub fn schedule(this: *@This()) void {
debug("{f} schedule", .{this});
WorkPool.schedule(&this.task);
}
pub fn runFromMainThread(this: *@This()) void {
debug("{f} runFromJS", .{this});
this.touch.onShellTouchTaskDone(this);
}
pub fn runFromMainThreadMini(this: *@This(), _: *void) void {
this.runFromMainThread();
}
fn runFromThreadPool(task: *jsc.WorkPoolTask) void {
var this: *ShellTouchTask = @fieldParentPtr("task", task);
debug("{f} runFromThreadPool", .{this});
// We have to give an absolute path
const filepath: [:0]const u8 = brk: {
if (ResolvePath.Platform.auto.isAbsolute(this.filepath)) break :brk this.filepath;
const parts: []const []const u8 = &.{
this.cwd_path[0..],
this.filepath[0..],
};
break :brk ResolvePath.joinZ(parts, .auto);
};
var node_fs = jsc.Node.fs.NodeFS{};
const milliseconds: f64 = @floatFromInt(std.time.milliTimestamp());
const atime: jsc.Node.TimeLike = if (bun.Environment.isWindows) milliseconds / 1000.0 else jsc.Node.TimeLike{
.sec = @intFromFloat(@divFloor(milliseconds, std.time.ms_per_s)),
.nsec = @intFromFloat(@mod(milliseconds, std.time.ms_per_s) * std.time.ns_per_ms),
};
const mtime = atime;
const args = jsc.Node.fs.Arguments.Utimes{
.atime = atime,
.mtime = mtime,
.path = .{ .string = bun.PathString.init(filepath) },
};
if (node_fs.utimes(args, .sync).asErr()) |err| out: {
if (err.getErrno() == .NOENT) {
const perm = 0o664;
switch (Syscall.open(filepath, bun.O.CREAT | bun.O.WRONLY, perm)) {
.result => |fd| {
fd.close();
break :out;
},
.err => |e| {
this.err = e.withPath(bun.handleOom(bun.default_allocator.dupe(u8, filepath))).toShellSystemError();
break :out;
},
}
}
this.err = err.withPath(bun.handleOom(bun.default_allocator.dupe(u8, filepath))).toShellSystemError();
}
if (this.event_loop == .js) {
this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(this, .manual_deinit));
} else {
this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(this, "runFromMainThreadMini"));
}
}
};
const Opts = struct {
/// -a
///
/// change only the access time
access_time_only: bool = false,
/// -c, --no-create
///
/// do not create any files
no_create: bool = false,
/// -d, --date=STRING
///
/// parse STRING and use it instead of current time
date: ?[]const u8 = null,
/// -h, --no-dereference
///
/// affect each symbolic link instead of any referenced file
/// (useful only on systems that can change the timestamps of a symlink)
no_dereference: bool = false,
/// -m
///
/// change only the modification time
modification_time_only: bool = false,
/// -r, --reference=FILE
///
/// use this file's times instead of current time
reference: ?[]const u8 = null,
/// -t STAMP
///
/// use [[CC]YY]MMDDhhmm[.ss] instead of current time
timestamp: ?[]const u8 = null,
/// --time=WORD
///
/// change the specified time:
/// WORD is access, atime, or use: equivalent to -a
/// WORD is modify or mtime: equivalent to -m
time: ?[]const u8 = null,
const Parse = FlagParser(*@This());
pub fn parse(opts: *Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) {
return Parse.parseFlags(opts, args);
}
pub fn parseLong(this: *Opts, flag: []const u8) ?ParseFlagResult {
_ = this;
if (bun.strings.eqlComptime(flag, "--no-create")) {
return .{
.unsupported = unsupportedFlag("--no-create"),
};
}
if (bun.strings.eqlComptime(flag, "--date")) {
return .{
.unsupported = unsupportedFlag("--date"),
};
}
if (bun.strings.eqlComptime(flag, "--reference")) {
return .{
.unsupported = unsupportedFlag("--reference=FILE"),
};
}
if (bun.strings.eqlComptime(flag, "--time")) {
return .{
.unsupported = unsupportedFlag("--reference=FILE"),
};
}
return null;
}
pub fn parseShort(this: *Opts, char: u8, smallflags: []const u8, i: usize) ?ParseFlagResult {
_ = this;
switch (char) {
'a' => {
return .{ .unsupported = unsupportedFlag("-a") };
},
'c' => {
return .{ .unsupported = unsupportedFlag("-c") };
},
'd' => {
return .{ .unsupported = unsupportedFlag("-d") };
},
'h' => {
return .{ .unsupported = unsupportedFlag("-h") };
},
'm' => {
return .{ .unsupported = unsupportedFlag("-m") };
},
'r' => {
return .{ .unsupported = unsupportedFlag("-r") };
},
't' => {
return .{ .unsupported = unsupportedFlag("-t") };
},
else => {
return .{ .illegal_option = smallflags[1 + i ..] };
},
}
return null;
}
};
pub inline fn bltn(this: *Touch) *Builtin {
const impl: *Builtin.Impl = @alignCast(@fieldParentPtr("touch", this));
return @fieldParentPtr("impl", impl);
}
// --
const debug = bun.Output.scoped(.ShellTouch, .hidden);
const log = debug;
const std = @import("std");
const interpreter = @import("../interpreter.zig");
const FlagParser = interpreter.FlagParser;
const Interpreter = interpreter.Interpreter;
const OutputSrc = interpreter.OutputSrc;
const OutputTask = interpreter.OutputTask;
const ParseError = interpreter.ParseError;
const ParseFlagResult = interpreter.ParseFlagResult;
const unsupportedFlag = interpreter.unsupportedFlag;
const Builtin = Interpreter.Builtin;
const Result = Interpreter.Builtin.Result;
const bun = @import("bun");
const ResolvePath = bun.path;
const Syscall = bun.sys;
const jsc = bun.jsc;
const WorkPool = bun.jsc.WorkPool;
const shell = bun.shell;
const ExitCode = shell.ExitCode;
const Yield = bun.shell.Yield;