also looking at this
Add Windows support and CI coverage
#24
6 files
+170
-266
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -28,10 +28,9 @@ fn getRealtimeClock() struct { sec: i64, nsec: i64 } { | ||
| 28 | 28 | /// Replaces std.time.nanoTimestamp() which was removed in Zig 0.16. |
| 29 | 29 | pub fn nanoTimestamp() i128 { |
| 30 | 30 | if (comptime builtin.os.tag == .windows) { |
| 31 | const ft = std.os.windows.GetSystemTimeAsFileTime(); | |
| 32 | const EPOCH_DIFF: i128 = 11644473600 * 1_000_000_000; | |
| 33 | const intervals: i128 = @as(i128, @as(u64, ft.dwHighDateTime) << 32 | @as(u64, ft.dwLowDateTime)); | |
| 34 | return intervals * 100 - EPOCH_DIFF; | |
| 31 | const intervals: i128 = @intCast(std.os.windows.ntdll.RtlGetSystemTimePrecise()); | |
| 32 | const epoch_ns: i128 = std.time.epoch.windows * std.time.ns_per_s; | |
| 33 | return intervals * 100 + epoch_ns; | |
| 35 | 34 | } else { |
| 36 | 35 | const clock = getRealtimeClock(); |
| 37 | 36 | return @as(i128, clock.sec) * 1_000_000_000 + @as(i128, clock.nsec); |
| @@ -41,15 +40,7 @@ pub fn nanoTimestamp() i128 { | ||
| 41 | 40 | /// Get current wall-clock time in milliseconds since Unix epoch. |
| 42 | 41 | /// Replaces std.time.milliTimestamp() which was removed in Zig 0.16. |
| 43 | 42 | pub fn milliTimestamp() i64 { |
| 44 | if (comptime builtin.os.tag == .windows) { | |
| 45 | const ft = std.os.windows.GetSystemTimeAsFileTime(); | |
| 46 | const EPOCH_DIFF: i64 = 11644473600000; | |
| 47 | const intervals: i64 = @bitCast(@as(u64, ft.dwHighDateTime) << 32 | @as(u64, ft.dwLowDateTime)); | |
| 48 | return @divFloor(intervals, 10000) - EPOCH_DIFF; | |
| 49 | } else { | |
| 50 | const clock = getRealtimeClock(); | |
| 51 | return @as(i64, clock.sec) * 1000 + @divFloor(@as(i64, clock.nsec), 1_000_000); | |
| 52 | } | |
| 43 | return @intCast(@divFloor(nanoTimestamp(), std.time.ns_per_ms)); | |
| 53 | 44 | } |
| 54 | 45 | |
| 55 | 46 | // ============================================================ |
| @@ -59,6 +50,12 @@ pub fn milliTimestamp() i64 { | ||
| 59 | 50 | /// Sleep for the given number of nanoseconds. |
| 60 | 51 | /// Replaces std.Thread.sleep() which was removed in Zig 0.16. |
| 61 | 52 | pub fn sleep(ns: u64) void { |
| 53 | if (comptime builtin.os.tag == .windows) { | |
| 54 | var interval: std.os.windows.LARGE_INTEGER = -@as(i64, @intCast(@max(ns / 100, 1))); | |
| 55 | _ = std.os.windows.ntdll.NtDelayExecution(.FALSE, &interval); | |
| 56 | return; | |
| 57 | } | |
| 58 | ||
| 62 | 59 | const s: isize = @intCast(ns / std.time.ns_per_s); |
| 63 | 60 | const remaining_ns: isize = @intCast(ns % std.time.ns_per_s); |
| 64 | 61 | var ts: std.c.timespec = .{ .sec = s, .nsec = remaining_ns }; |
| @@ -97,123 +94,58 @@ pub const Mutex = struct { | ||
| 97 | 94 | }; |
| 98 | 95 | |
| 99 | 96 | // ============================================================ |
| 100 | // File descriptor close wrapper | |
| 97 | // File I/O helpers (std.fs.cwd() removed, needs std.Io now) | |
| 101 | 98 | // ============================================================ |
| 102 | 99 | |
| 103 | /// Close a file descriptor using libc. | |
| 104 | /// Replaces std.posix.close() which was removed in Zig 0.16. | |
| 105 | fn closeFd(fd: std.posix.fd_t) void { | |
| 106 | _ = std.c.close(fd); | |
| 100 | fn initIo(allocator: std.mem.Allocator) std.Io.Threaded { | |
| 101 | return .init(allocator, .{ .environ = currentEnviron() }); | |
| 107 | 102 | } |
| 108 | 103 | |
| 109 | // ============================================================ | |
| 110 | // File I/O helpers (std.fs.cwd() removed, needs std.Io now) | |
| 111 | // ============================================================ | |
| 104 | fn currentEnviron() std.process.Environ { | |
| 105 | return if (comptime builtin.os.tag == .windows) | |
| 106 | .{ .block = .global } | |
| 107 | else | |
| 108 | .{ .block = .{ .slice = std.mem.span(std.c.environ) } }; | |
| 109 | } | |
| 112 | 110 | |
| 113 | /// Read entire file contents using POSIX APIs. | |
| 111 | /// Read entire file contents using portable Zig I/O. | |
| 114 | 112 | /// Replaces std.fs.cwd().openFile() + file.readToEndAlloc(). |
| 115 | 113 | pub fn readFileAlloc(allocator: std.mem.Allocator, path: []const u8) ![]const u8 { |
| 116 | const path_z = try allocator.dupeSentinel(u8, path, 0); | |
| 117 | defer allocator.free(path_z); | |
| 118 | ||
| 119 | const fd = std.posix.openatZ(std.posix.AT.FDCWD, path_z, .{}, 0) catch |err| { | |
| 120 | if (err == error.FileNotFound) return error.FileNotFound; | |
| 121 | return err; | |
| 122 | }; | |
| 123 | defer closeFd(fd); | |
| 124 | ||
| 125 | // Read file in chunks | |
| 126 | var result = std.ArrayList(u8).empty; | |
| 127 | errdefer result.deinit(allocator); | |
| 128 | ||
| 129 | var buf: [4096]u8 = undefined; | |
| 130 | while (true) { | |
| 131 | const n = std.posix.read(fd, &buf) catch |err| return err; | |
| 132 | if (n == 0) break; | |
| 133 | try result.appendSlice(allocator, buf[0..n]); | |
| 134 | } | |
| 135 | ||
| 136 | return result.toOwnedSlice(allocator); | |
| 114 | var threaded = initIo(allocator); | |
| 115 | defer threaded.deinit(); | |
| 116 | return std.Io.Dir.cwd().readFileAlloc(threaded.io(), path, allocator, .unlimited); | |
| 137 | 117 | } |
| 138 | 118 | |
| 139 | /// Write content to a file using POSIX APIs. | |
| 119 | /// Write content to a file using portable Zig I/O. | |
| 140 | 120 | /// Replaces std.fs.cwd().createFile() + file.writeAll(). |
| 141 | 121 | pub fn writeFile(allocator: std.mem.Allocator, path: []const u8, content: []const u8) !void { |
| 142 | const path_z = try allocator.dupeSentinel(u8, path, 0); | |
| 143 | defer allocator.free(path_z); | |
| 144 | ||
| 145 | const fd = std.posix.openatZ(std.posix.AT.FDCWD, path_z, .{ | |
| 146 | .ACCMODE = .WRONLY, | |
| 147 | .CREAT = true, | |
| 148 | .TRUNC = true, | |
| 149 | }, 0o644) catch |err| return err; | |
| 150 | defer closeFd(fd); | |
| 151 | ||
| 152 | var remaining = content; | |
| 153 | while (remaining.len > 0) { | |
| 154 | const rc = std.c.write(fd, remaining.ptr, remaining.len); | |
| 155 | if (rc < 0) return error.Unexpected; | |
| 156 | const written: usize = @intCast(rc); | |
| 157 | remaining = remaining[written..]; | |
| 158 | } | |
| 122 | var threaded = initIo(allocator); | |
| 123 | defer threaded.deinit(); | |
| 124 | try std.Io.Dir.cwd().writeFile(threaded.io(), .{ .sub_path = path, .data = content }); | |
| 159 | 125 | } |
| 160 | 126 | |
| 161 | /// Delete a file using POSIX APIs. | |
| 127 | /// Delete a file using portable Zig I/O. | |
| 162 | 128 | /// Replaces std.fs.cwd().deleteFile(). |
| 163 | 129 | pub fn deleteFile(allocator: std.mem.Allocator, path: []const u8) !void { |
| 164 | const path_z = try allocator.dupeSentinel(u8, path, 0); | |
| 165 | defer allocator.free(path_z); | |
| 166 | const rc = std.c.unlink(path_z); | |
| 167 | if (rc != 0) { | |
| 168 | switch (std.c.errno(rc)) { | |
| 169 | .NOENT => return error.FileNotFound, | |
| 170 | else => return error.Unexpected, | |
| 171 | } | |
| 172 | } | |
| 130 | var threaded = initIo(allocator); | |
| 131 | defer threaded.deinit(); | |
| 132 | try std.Io.Dir.cwd().deleteFile(threaded.io(), path); | |
| 173 | 133 | } |
| 174 | 134 | |
| 175 | /// Delete an empty directory using POSIX APIs. | |
| 135 | /// Delete an empty directory using portable Zig I/O. | |
| 176 | 136 | /// Replaces std.fs.cwd().deleteDir(). |
| 177 | 137 | pub fn deleteDir(allocator: std.mem.Allocator, path: []const u8) !void { |
| 178 | const path_z = try allocator.dupeSentinel(u8, path, 0); | |
| 179 | defer allocator.free(path_z); | |
| 180 | const rc = std.c.rmdir(path_z); | |
| 181 | if (rc != 0) { | |
| 182 | switch (std.c.errno(rc)) { | |
| 183 | .NOENT => return error.FileNotFound, | |
| 184 | else => return error.Unexpected, | |
| 185 | } | |
| 186 | } | |
| 138 | var threaded = initIo(allocator); | |
| 139 | defer threaded.deinit(); | |
| 140 | try std.Io.Dir.cwd().deleteDir(threaded.io(), path); | |
| 187 | 141 | } |
| 188 | 142 | |
| 189 | /// Create directories recursively using POSIX APIs. | |
| 143 | /// Create directories recursively using portable Zig I/O. | |
| 190 | 144 | /// Replaces std.fs.cwd().makePath(). |
| 191 | 145 | pub fn makePath(allocator: std.mem.Allocator, path: []const u8) !void { |
| 192 | const path_z = try allocator.dupeSentinel(u8, path, 0); | |
| 193 | defer allocator.free(path_z); | |
| 194 | ||
| 195 | const rc = std.c.mkdir(path_z, 0o755); | |
| 196 | if (rc == 0) return; | |
| 197 | ||
| 198 | switch (std.c.errno(rc)) { | |
| 199 | .EXIST => return, // Already exists | |
| 200 | .NOENT => { | |
| 201 | // Parent doesn't exist, find and create it | |
| 202 | if (std.mem.lastIndexOfScalar(u8, path, '/')) |sep| { | |
| 203 | if (sep > 0) { | |
| 204 | try makePath(allocator, path[0..sep]); | |
| 205 | // Retry creating the directory | |
| 206 | const retry_z = try allocator.dupeSentinel(u8, path, 0); | |
| 207 | defer allocator.free(retry_z); | |
| 208 | const rc2 = std.c.mkdir(retry_z, 0o755); | |
| 209 | if (rc2 != 0 and std.c.errno(rc2) != .EXIST) { | |
| 210 | return error.Unexpected; | |
| 211 | } | |
| 212 | } | |
| 213 | } | |
| 214 | }, | |
| 215 | else => return error.Unexpected, | |
| 216 | } | |
| 146 | var threaded = initIo(allocator); | |
| 147 | defer threaded.deinit(); | |
| 148 | try std.Io.Dir.cwd().createDirPath(threaded.io(), path); | |
| 217 | 149 | } |
| 218 | 150 | |
| 219 | 151 | // ============================================================ |
| @@ -226,56 +158,44 @@ pub const DirEntry = struct { | ||
| 226 | 158 | kind: enum { file, directory, sym_link, other }, |
| 227 | 159 | }; |
| 228 | 160 | |
| 229 | /// A simple directory iterator using POSIX APIs. | |
| 161 | /// A simple directory iterator using portable Zig I/O. | |
| 230 | 162 | /// Replaces std.fs.openDirAbsolute() + dir.iterate() which was removed in Zig 0.16. |
| 231 | 163 | pub const DirIterator = struct { |
| 232 | dir: *std.c.DIR, | |
| 233 | path_buf: [1024]u8, | |
| 164 | threaded: *std.Io.Threaded, | |
| 165 | dir: std.Io.Dir, | |
| 166 | iterator: std.Io.Dir.Iterator, | |
| 234 | 167 | |
| 235 | 168 | pub fn open(dir_path: []const u8) !DirIterator { |
| 236 | var path_buf: [1024:0]u8 = @splat(0); | |
| 237 | if (dir_path.len >= path_buf.len) return error.NameTooLong; | |
| 238 | @memcpy(path_buf[0..dir_path.len], dir_path); | |
| 169 | const threaded = try std.heap.page_allocator.create(std.Io.Threaded); | |
| 170 | errdefer std.heap.page_allocator.destroy(threaded); | |
| 171 | threaded.* = initIo(std.heap.page_allocator); | |
| 172 | errdefer threaded.deinit(); | |
| 239 | 173 | |
| 240 | const dir = std.c.opendir(&path_buf); | |
| 241 | if (dir == null) return error.FileNotFound; | |
| 174 | const dir = try std.Io.Dir.cwd().openDir(threaded.io(), dir_path, .{ .iterate = true }); | |
| 242 | 175 | return .{ |
| 243 | .dir = dir.?, | |
| 244 | .path_buf = undefined, | |
| 176 | .threaded = threaded, | |
| 177 | .dir = dir, | |
| 178 | .iterator = dir.iterateAssumeFirstIteration(), | |
| 245 | 179 | }; |
| 246 | 180 | } |
| 247 | 181 | |
| 248 | 182 | pub fn next(self: *DirIterator) !?DirEntry { |
| 249 | while (true) { | |
| 250 | const entry_opt = std.c.readdir(self.dir); | |
| 251 | if (entry_opt == null) return null; | |
| 252 | const entry = entry_opt.?; | |
| 253 | ||
| 254 | // Get name as slice | |
| 255 | const name_ptr: [*]const u8 = @ptrCast(&entry.name); | |
| 256 | const name_len = if (@hasField(@TypeOf(entry.*), "namlen")) | |
| 257 | entry.namlen | |
| 258 | else | |
| 259 | std.mem.indexOfScalar(u8, name_ptr[0..256], 0) orelse 256; | |
| 260 | const name = name_ptr[0..name_len]; | |
| 261 | ||
| 262 | // Skip . and .. | |
| 263 | if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) continue; | |
| 264 | ||
| 265 | return .{ | |
| 266 | .name = name, | |
| 267 | .kind = switch (entry.type) { | |
| 268 | std.c.DT.DIR => .directory, | |
| 269 | std.c.DT.REG => .file, | |
| 270 | std.c.DT.LNK => .sym_link, | |
| 271 | else => .other, | |
| 272 | }, | |
| 273 | }; | |
| 274 | } | |
| 183 | const entry = try self.iterator.next(self.threaded.io()) orelse return null; | |
| 184 | return .{ | |
| 185 | .name = entry.name, | |
| 186 | .kind = switch (entry.kind) { | |
| 187 | .directory => .directory, | |
| 188 | .file => .file, | |
| 189 | .sym_link => .sym_link, | |
| 190 | else => .other, | |
| 191 | }, | |
| 192 | }; | |
| 275 | 193 | } |
| 276 | 194 | |
| 277 | 195 | pub fn close(self: *DirIterator) void { |
| 278 | _ = std.c.closedir(self.dir); | |
| 196 | self.dir.close(self.threaded.io()); | |
| 197 | self.threaded.deinit(); | |
| 198 | std.heap.page_allocator.destroy(self.threaded); | |
| 279 | 199 | } |
| 280 | 200 | }; |
| 281 | 201 | |
| @@ -298,7 +218,8 @@ pub const SpawnResult = union(enum) { | ||
| 298 | 218 | }; |
| 299 | 219 | |
| 300 | 220 | /// Spawn a child process and wait for it to complete. |
| 301 | /// Replaces std.process.Child.init() + spawnAndWait() which was removed in Zig 0.16. | |
| 221 | /// Uses Zig's portable process API, including PATH lookup and Windows process | |
| 222 | /// creation, while preserving the compatibility result used by callers. | |
| 302 | 223 | pub fn spawnAndWait( |
| 303 | 224 | allocator: std.mem.Allocator, |
| 304 | 225 | argv: []const []const u8, |
| @@ -307,105 +228,30 @@ pub fn spawnAndWait( | ||
| 307 | 228 | ) !SpawnResult { |
| 308 | 229 | if (argv.len == 0) return error.InvalidArgument; |
| 309 | 230 | |
| 310 | // Convert argv to null-terminated C strings | |
| 311 | const c_argv = try allocator.alloc(?[*:0]const u8, argv.len + 1); | |
| 312 | defer allocator.free(c_argv); | |
| 313 | ||
| 314 | for (argv, 0..) |arg, i| { | |
| 315 | const z = try allocator.dupeSentinel(u8, arg, 0); | |
| 316 | c_argv[i] = z.ptr; | |
| 317 | } | |
| 318 | c_argv[argv.len] = null; | |
| 319 | ||
| 320 | // Fork | |
| 321 | const pid = std.c.fork(); | |
| 322 | if (pid < 0) return error.ForkFailed; | |
| 323 | ||
| 324 | if (pid == 0) { | |
| 325 | // Child process | |
| 326 | // Handle stdout | |
| 327 | switch (stdout_behavior) { | |
| 328 | .Ignore => { | |
| 329 | const dev_null: [*:0]const u8 = "/dev/null"; | |
| 330 | const null_fd = std.posix.openatZ(std.posix.AT.FDCWD, dev_null, .{ .ACCMODE = .WRONLY }, 0) catch std.process.exit(127); | |
| 331 | if (std.c.dup2(null_fd, 1) < 0) std.process.exit(127); | |
| 332 | closeFd(null_fd); | |
| 333 | }, | |
| 334 | else => {}, | |
| 335 | } | |
| 336 | ||
| 337 | // Handle stderr | |
| 338 | switch (stderr_behavior) { | |
| 339 | .Ignore => { | |
| 340 | const dev_null: [*:0]const u8 = "/dev/null"; | |
| 341 | const null_fd = std.posix.openatZ(std.posix.AT.FDCWD, dev_null, .{ .ACCMODE = .WRONLY }, 0) catch std.process.exit(127); | |
| 342 | if (std.c.dup2(null_fd, 2) < 0) std.process.exit(127); | |
| 343 | closeFd(null_fd); | |
| 344 | }, | |
| 345 | else => {}, | |
| 346 | } | |
| 347 | ||
| 348 | // Exec - use execve with PATH lookup and current environment | |
| 349 | const envp: [*:null]const ?[*:0]const u8 = @ptrCast(std.c.environ); | |
| 350 | ||
| 351 | // Try direct execution first | |
| 352 | _ = std.c.execve(c_argv[0].?, @ptrCast(c_argv.ptr), envp); | |
| 353 | ||
| 354 | // If direct exec failed, try PATH lookup | |
| 355 | const cmd = std.mem.sliceTo(c_argv[0].?, 0); | |
| 356 | if (std.mem.indexOfScalar(u8, cmd, '/') == null) { | |
| 357 | // Search PATH | |
| 358 | var path_search: ?[*:0]const u8 = null; | |
| 359 | var env_idx: usize = 0; | |
| 360 | while (std.c.environ[env_idx]) |env_entry| : (env_idx += 1) { | |
| 361 | const entry = std.mem.sliceTo(env_entry, 0); | |
| 362 | if (std.mem.startsWith(u8, entry, "PATH=")) { | |
| 363 | path_search = @ptrCast(env_entry + 5); | |
| 364 | break; | |
| 365 | } | |
| 366 | } | |
| 367 | ||
| 368 | if (path_search) |path_val| { | |
| 369 | const path_str = std.mem.sliceTo(path_val, 0); | |
| 370 | var path_iter = std.mem.splitScalar(u8, path_str, ':'); | |
| 371 | while (path_iter.next()) |dir| { | |
| 372 | var full_path: [1024:0]u8 = @splat(0); | |
| 373 | if (dir.len + 1 + cmd.len < full_path.len) { | |
| 374 | @memcpy(full_path[0..dir.len], dir); | |
| 375 | full_path[dir.len] = '/'; | |
| 376 | @memcpy(full_path[dir.len + 1 ..][0..cmd.len], cmd); | |
| 377 | _ = std.c.execve(&full_path, @ptrCast(c_argv.ptr), envp); | |
| 378 | } | |
| 379 | } | |
| 380 | } | |
| 381 | } | |
| 382 | std.process.exit(127); | |
| 383 | } | |
| 384 | ||
| 385 | // Parent process - free the duped strings | |
| 386 | for (argv, 0..) |_, i| { | |
| 387 | const z: [*:0]const u8 = c_argv[i].?; | |
| 388 | const len = std.mem.len(z); | |
| 389 | allocator.free(z[0 .. len + 1]); | |
| 390 | } | |
| 391 | ||
| 392 | // Wait for child | |
| 393 | var status: c_int = 0; | |
| 394 | const wait_result = std.c.waitpid(pid, &status, 0); | |
| 395 | if (wait_result < 0) return error.WaitFailed; | |
| 396 | ||
| 397 | const ustatus: u32 = @bitCast(status); | |
| 398 | ||
| 399 | // Check if exited normally (WIFEXITED) | |
| 400 | if (ustatus & 0x7f == 0) { | |
| 401 | // WEXITSTATUS | |
| 402 | const exit_code: u8 = @intCast((ustatus >> 8) & 0xff); | |
| 403 | return .{ .Exited = exit_code }; | |
| 404 | } | |
| 231 | var threaded = initIo(allocator); | |
| 232 | defer threaded.deinit(); | |
| 233 | const io = threaded.io(); | |
| 234 | ||
| 235 | var child = try std.process.spawn(io, .{ | |
| 236 | .argv = argv, | |
| 237 | .stdout = stdBehavior(stdout_behavior), | |
| 238 | .stderr = stdBehavior(stderr_behavior), | |
| 239 | .create_no_window = false, | |
| 240 | }); | |
| 241 | const term = try child.wait(io); | |
| 242 | return switch (term) { | |
| 243 | .exited => |code| .{ .Exited = code }, | |
| 244 | .signal => |signal| .{ .Signal = @backingInt(signal) }, | |
| 245 | .stopped => |signal| .{ .Signal = @backingInt(signal) }, | |
| 246 | .unknown => |status| .{ .Unknown = status }, | |
| 247 | }; | |
| 248 | } | |
| 405 | 249 | |
| 406 | // Signal | |
| 407 | const sig = ustatus & 0x7f; | |
| 408 | return .{ .Signal = sig }; | |
| 250 | fn stdBehavior(behavior: StdBehavior) std.process.SpawnOptions.StdIo { | |
| 251 | return switch (behavior) { | |
| 252 | .Inherit, .Pipe => .inherit, | |
| 253 | .Ignore => .ignore, | |
| 254 | }; | |
| 409 | 255 | } |
| 410 | 256 | |
| 411 | 257 | // ============================================================ |
| @@ -452,10 +298,33 @@ pub const ArrayListWriter = struct { | ||
| 452 | 298 | } |
| 453 | 299 | }; |
| 454 | 300 | |
| 455 | // ============================================================ | |
| 456 | // Signal handler type | |
| 457 | // ============================================================ | |
| 301 | test "portable file helpers preserve nested paths" { | |
| 302 | const allocator = std.testing.allocator; | |
| 303 | const nonce = nanoTimestamp(); | |
| 304 | const dir_path = try std.fmt.allocPrint(allocator, ".zig-cache/compat-{d}", .{nonce}); | |
| 305 | defer allocator.free(dir_path); | |
| 306 | const file_path = try std.fs.path.join(allocator, &.{ dir_path, "roundtrip.txt" }); | |
| 307 | defer allocator.free(file_path); | |
| 308 | ||
| 309 | try makePath(allocator, dir_path); | |
| 310 | defer deleteDir(allocator, dir_path) catch {}; | |
| 311 | defer deleteFile(allocator, file_path) catch {}; | |
| 312 | ||
| 313 | try writeFile(allocator, file_path, "portable"); | |
| 314 | const contents = try readFileAlloc(allocator, file_path); | |
| 315 | defer allocator.free(contents); | |
| 316 | try std.testing.expectEqualStrings("portable", contents); | |
| 317 | } | |
| 458 | 318 | |
| 459 | /// The signal type used in signal handlers. | |
| 460 | /// In Zig 0.16, this changed from c_int to std.posix.SIG enum. | |
| 461 | pub const SignalType = std.posix.SIG; | |
| 319 | test "portable process helper preserves exit codes" { | |
| 320 | const argv: []const []const u8 = if (comptime builtin.os.tag == .windows) | |
| 321 | &.{ "cmd.exe", "/C", "exit", "7" } | |
| 322 | else | |
| 323 | &.{ "sh", "-c", "exit 7" }; | |
| 324 | ||
| 325 | const result = try spawnAndWait(std.testing.allocator, argv, .Ignore, .Ignore); | |
| 326 | switch (result) { | |
| 327 | .Exited => |code| try std.testing.expectEqual(@as(u8, 7), code), | |
| 328 | else => return error.UnexpectedTermination, | |
| 329 | } | |
| 330 | } | |
| @@ -119,11 +119,7 @@ fn scanDirectory( | ||
| 119 | 119 | else |
| 120 | 120 | full_path; |
| 121 | 121 | |
| 122 | // Skip leading slash in relative path | |
| 123 | const clean_relative = if (relative_path.len > 0 and relative_path[0] == '/') | |
| 124 | relative_path[1..] | |
| 125 | else | |
| 126 | relative_path; | |
| 122 | const clean_relative = cleanRelativePath(relative_path); | |
| 127 | 123 | |
| 128 | 124 | try result.addFile(full_path, clean_relative, entry.name); |
| 129 | 125 | } |
| @@ -135,6 +131,15 @@ fn scanDirectory( | ||
| 135 | 131 | } |
| 136 | 132 | } |
| 137 | 133 | |
| 134 | /// Removes separators introduced by slicing the root prefix. Accept both | |
| 135 | /// spellings so paths remain stable when a project moves between POSIX and | |
| 136 | /// Windows hosts. | |
| 137 | fn cleanRelativePath(path: []const u8) []const u8 { | |
| 138 | var start: usize = 0; | |
| 139 | while (start < path.len and (path[start] == '/' or path[start] == '\\')) : (start += 1) {} | |
| 140 | return path[start..]; | |
| 141 | } | |
| 142 | ||
| 138 | 143 | /// Check if a filename matches the pattern |
| 139 | 144 | fn matchesPattern(filename: []const u8, pattern: []const u8) bool { |
| 140 | 145 | // Simple pattern matching - check if filename ends with pattern |
| @@ -160,3 +165,8 @@ test "matchesPattern with exact match" { | ||
| 160 | 165 | try std.testing.expect(matchesPattern("test.zig", "test.zig")); |
| 161 | 166 | try std.testing.expect(!matchesPattern("other.zig", "test.zig")); |
| 162 | 167 | } |
| 168 | ||
| 169 | test "relative paths discard POSIX and Windows separators" { | |
| 170 | try std.testing.expectEqualStrings("suite/sample.test.zig", cleanRelativePath("/suite/sample.test.zig")); | |
| 171 | try std.testing.expectEqualStrings("suite\\sample.test.zig", cleanRelativePath("\\suite\\sample.test.zig")); | |
| 172 | } | |
| @@ -1,24 +1,30 @@ | ||
| 1 | 1 | const std = ; |
| 2 | const builtin = ; | |
| 2 | 3 | const lib = ; |
| 3 | 4 | const compat = lib.compat; |
| 4 | 5 | |
| 5 | 6 | // Global signal handler state |
| 6 | 7 | var shutdown_requested = std.atomic.Value(bool).init(false); |
| 7 | 8 | |
| 8 | /// Signal handler for SIGINT and SIGTERM | |
| 9 | fn handleSignal(sig: std.posix.SIG) callconv(.c) void { | |
| 10 | _ = sig; | |
| 11 | shutdown_requested.store(true, .monotonic); | |
| 12 | std.debug.print("\n\nShutdown requested... cleaning up\n", .{}); | |
| 13 | } | |
| 14 | ||
| 15 | 9 | /// Install signal handlers |
| 16 | 10 | fn installSignalHandlers() !void { |
| 11 | // Windows console control events retain their native default behavior: | |
| 12 | // Ctrl-C and console close terminate the CLI. POSIX platforms install | |
| 13 | // cooperative handlers so long-running watch/UI modes can clean up. | |
| 14 | if (comptime builtin.os.tag == .windows) return; | |
| 15 | ||
| 16 | const Handler = struct { | |
| 17 | fn handle(sig: std.posix.SIG) callconv(.c) void { | |
| 18 | _ = sig; | |
| 19 | shutdown_requested.store(true, .monotonic); | |
| 20 | std.debug.print("\n\nShutdown requested... cleaning up\n", .{}); | |
| 21 | } | |
| 22 | }; | |
| 17 | 23 | const posix = std.posix; |
| 18 | 24 | |
| 19 | 25 | // Install SIGINT handler (Ctrl+C) |
| 20 | 26 | const sigint_action = posix.Sigaction{ |
| 21 | .handler = .{ .handler = handleSignal }, | |
| 27 | .handler = .{ .handler = Handler.handle }, | |
| 22 | 28 | .mask = posix.sigemptyset(), |
| 23 | 29 | .flags = 0, |
| 24 | 30 | }; |
| @@ -26,7 +32,7 @@ fn installSignalHandlers() !void { | ||
| 26 | 32 | |
| 27 | 33 | // Install SIGTERM handler |
| 28 | 34 | const sigterm_action = posix.Sigaction{ |
| 29 | .handler = .{ .handler = handleSignal }, | |
| 35 | .handler = .{ .handler = Handler.handle }, | |
| 30 | 36 | .mask = posix.sigemptyset(), |
| 31 | 37 | .flags = 0, |
| 32 | 38 | }; |
| @@ -52,7 +58,8 @@ pub fn main(init: std.process.Init.Minimal) !void { | ||
| 52 | 58 | for (args_list.items) |arg| allocator.free(arg); |
| 53 | 59 | args_list.deinit(allocator); |
| 54 | 60 | } |
| 55 | var args_iter = std.process.Args.Iterator.init(init.args); | |
| 61 | var args_iter = try std.process.Args.Iterator.initAllocator(init.args, allocator); | |
| 62 | defer args_iter.deinit(); | |
| 56 | 63 | while (args_iter.next()) |arg| { |
| 57 | 64 | try args_list.append(allocator, try allocator.dupe(u8, arg)); |
| 58 | 65 | } |