authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-31 15:06:13-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-04 00:27:08-08:00
log3e6d6150d98658e6c46ad5c378f90fb628f97d2a
tree0cbbcc777c632c6c241aaf5072254df40e0c2bc2
parent384bfc5f99d0d46521d62e09858facc2edd3bc74

std.process.Environ: fix compile errors on POSIX


8 files changed, 267 insertions(+), 227 deletions(-)

lib/compiler/test_runner.zig+2-3
...@@ -29,7 +29,7 @@ const need_simple = switch (builtin.zig_backend) {...@@ -29,7 +29,7 @@ const need_simple = switch (builtin.zig_backend) {
29 else => false,29 else => false,
30};30};
3131
32pub fn main() void {32pub fn main(init: std.process.Init.Minimal) void {
33 @disableInstrumentation();33 @disableInstrumentation();
3434
35 if (builtin.cpu.arch.isSpirV()) {35 if (builtin.cpu.arch.isSpirV()) {
...@@ -41,8 +41,7 @@ pub fn main() void {...@@ -41,8 +41,7 @@ pub fn main() void {
41 return mainSimple() catch @panic("test failure\n");41 return mainSimple() catch @panic("test failure\n");
42 }42 }
4343
44 const args = std.process.argsAlloc(fba.allocator()) catch44 const args = init.args.toSlice(fba.allocator()) catch @panic("unable to parse command line args");
45 @panic("unable to parse command line args");
4645
47 var listen = false;46 var listen = false;
48 var opt_cache_dir: ?[]const u8 = null;47 var opt_cache_dir: ?[]const u8 = null;
lib/std/Io/Threaded.zig+2-2
...@@ -12836,11 +12836,11 @@ fn processSpawnPosix(userdata: ?*anyopaque, options: process.SpawnOptions) proce...@@ -12836,11 +12836,11 @@ fn processSpawnPosix(userdata: ?*anyopaque, options: process.SpawnOptions) proce
12836 const envp: [*:null]const ?[*:0]const u8 = m: {12836 const envp: [*:null]const ?[*:0]const u8 = m: {
12837 const prog_fd: i32 = if (prog_pipe[1] == -1) -1 else prog_fileno;12837 const prog_fd: i32 = if (prog_pipe[1] == -1) -1 else prog_fileno;
12838 if (options.env_map) |env_map| {12838 if (options.env_map) |env_map| {
12839 break :m (try env_map.createBlock(arena, .{12839 break :m (try env_map.createBlockPosix(arena, .{
12840 .zig_progress_fd = prog_fd,12840 .zig_progress_fd = prog_fd,
12841 })).ptr;12841 })).ptr;
12842 }12842 }
12843 break :m (try process.Environ.createBlock(.{ .block = t.environ.block }, arena, .{12843 break :m (try process.Environ.createBlockPosix(.{ .block = t.environ.block }, arena, .{
12844 .zig_progress_fd = prog_fd,12844 .zig_progress_fd = prog_fd,
12845 })).ptr;12845 })).ptr;
12846 };12846 };
lib/std/dynamic_library.zig+18-10
...@@ -31,12 +31,20 @@ pub const DynLib = struct {...@@ -31,12 +31,20 @@ pub const DynLib = struct {
3131
32 /// Trusts the file. Malicious file will be able to execute arbitrary code.32 /// Trusts the file. Malicious file will be able to execute arbitrary code.
33 pub fn open(path: []const u8) Error!DynLib {33 pub fn open(path: []const u8) Error!DynLib {
34 return .{ .inner = try InnerType.open(path) };34 if (InnerType == ElfDynLib) {
35 return .{ .inner = try InnerType.open(path, null) };
36 } else {
37 return .{ .inner = try InnerType.open(path) };
38 }
35 }39 }
3640
37 /// Trusts the file. Malicious file will be able to execute arbitrary code.41 /// Trusts the file. Malicious file will be able to execute arbitrary code.
38 pub fn openZ(path_c: [*:0]const u8) Error!DynLib {42 pub fn openZ(path_c: [*:0]const u8) Error!DynLib {
39 return .{ .inner = try InnerType.openZ(path_c) };43 if (InnerType == ElfDynLib) {
44 return .{ .inner = try InnerType.openZ(path_c, null) };
45 } else {
46 return .{ .inner = try InnerType.openZ(path_c) };
47 }
40 }48 }
4149
42 /// Trusts the file.50 /// Trusts the file.
...@@ -197,7 +205,7 @@ pub const ElfDynLib = struct {...@@ -197,7 +205,7 @@ pub const ElfDynLib = struct {
197 // - DT_RPATH of the calling binary is not used as a search path205 // - DT_RPATH of the calling binary is not used as a search path
198 // - DT_RUNPATH of the calling binary is not used as a search path206 // - DT_RUNPATH of the calling binary is not used as a search path
199 // - /etc/ld.so.cache is not read207 // - /etc/ld.so.cache is not read
200 fn resolveFromName(io: Io, path_or_name: []const u8) !posix.fd_t {208 fn resolveFromName(io: Io, path_or_name: []const u8, LD_LIBRARY_PATH: ?[]const u8) !posix.fd_t {
201 // If filename contains a slash ("/"), then it is interpreted as a (relative or absolute) pathname209 // If filename contains a slash ("/"), then it is interpreted as a (relative or absolute) pathname
202 if (std.mem.findScalarPos(u8, path_or_name, 0, '/')) |_| {210 if (std.mem.findScalarPos(u8, path_or_name, 0, '/')) |_| {
203 return posix.open(path_or_name, .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0);211 return posix.open(path_or_name, .{ .ACCMODE = .RDONLY, .CLOEXEC = true }, 0);
...@@ -207,7 +215,7 @@ pub const ElfDynLib = struct {...@@ -207,7 +215,7 @@ pub const ElfDynLib = struct {
207 if (std.os.linux.geteuid() == std.os.linux.getuid() and215 if (std.os.linux.geteuid() == std.os.linux.getuid() and
208 std.os.linux.getegid() == std.os.linux.getgid())216 std.os.linux.getegid() == std.os.linux.getgid())
209 {217 {
210 if (posix.getenvZ("LD_LIBRARY_PATH")) |ld_library_path| {218 if (LD_LIBRARY_PATH) |ld_library_path| {
211 if (resolveFromSearchPath(io, ld_library_path, path_or_name, ':')) |fd| {219 if (resolveFromSearchPath(io, ld_library_path, path_or_name, ':')) |fd| {
212 return fd;220 return fd;
213 }221 }
...@@ -221,10 +229,10 @@ pub const ElfDynLib = struct {...@@ -221,10 +229,10 @@ pub const ElfDynLib = struct {
221 }229 }
222230
223 /// Trusts the file. Malicious file will be able to execute arbitrary code.231 /// Trusts the file. Malicious file will be able to execute arbitrary code.
224 pub fn open(path: []const u8) Error!ElfDynLib {232 pub fn open(path: []const u8, LD_LIBRARY_PATH: ?[]const u8) Error!ElfDynLib {
225 const io = std.Options.debug_io;233 const io = std.Options.debug_io;
226234
227 const fd = try resolveFromName(io, path);235 const fd = try resolveFromName(io, path, LD_LIBRARY_PATH);
228 defer posix.close(fd);236 defer posix.close(fd);
229237
230 const file: Io.File = .{ .handle = fd };238 const file: Io.File = .{ .handle = fd };
...@@ -371,8 +379,8 @@ pub const ElfDynLib = struct {...@@ -371,8 +379,8 @@ pub const ElfDynLib = struct {
371 }379 }
372380
373 /// Trusts the file. Malicious file will be able to execute arbitrary code.381 /// Trusts the file. Malicious file will be able to execute arbitrary code.
374 pub fn openZ(path_c: [*:0]const u8) Error!ElfDynLib {382 pub fn openZ(path_c: [*:0]const u8, LD_LIBRARY_PATH: ?[]const u8) Error!ElfDynLib {
375 return open(mem.sliceTo(path_c, 0));383 return open(mem.sliceTo(path_c, 0), LD_LIBRARY_PATH);
376 }384 }
377385
378 /// Trusts the file386 /// Trusts the file
...@@ -554,8 +562,8 @@ fn checkver(def_arg: *elf.Verdef, vsym_arg: elf.Versym, vername: []const u8, str...@@ -554,8 +562,8 @@ fn checkver(def_arg: *elf.Verdef, vsym_arg: elf.Versym, vername: []const u8, str
554562
555test "ElfDynLib" {563test "ElfDynLib" {
556 if (native_os != .linux) return error.SkipZigTest;564 if (native_os != .linux) return error.SkipZigTest;
557 try testing.expectError(error.FileNotFound, ElfDynLib.open("invalid_so.so"));565 try testing.expectError(error.FileNotFound, ElfDynLib.open("invalid_so.so", null));
558 try testing.expectError(error.FileNotFound, ElfDynLib.openZ("invalid_so.so"));566 try testing.expectError(error.FileNotFound, ElfDynLib.openZ("invalid_so.so", null));
559}567}
560568
561/// Separated to avoid referencing `WindowsDynLib`, because its field types may not569/// Separated to avoid referencing `WindowsDynLib`, because its field types may not
lib/std/posix/test.zig+1-6
...@@ -140,11 +140,6 @@ test "pipe" {...@@ -140,11 +140,6 @@ test "pipe" {
140 posix.close(fds[0]);140 posix.close(fds[0]);
141}141}
142142
143test "argsAlloc" {
144 const args = try std.process.argsAlloc(std.testing.allocator);
145 std.process.argsFree(std.testing.allocator, args);
146}
147
148test "memfd_create" {143test "memfd_create" {
149 const io = testing.io;144 const io = testing.io;
150145
...@@ -473,7 +468,7 @@ test "getpid" {...@@ -473,7 +468,7 @@ test "getpid" {
473 if (native_os == .wasi) return error.SkipZigTest;468 if (native_os == .wasi) return error.SkipZigTest;
474 if (native_os == .windows) return error.SkipZigTest;469 if (native_os == .windows) return error.SkipZigTest;
475470
476 try expect(posix.getpid() != 0);471 try expect(posix.system.getpid() != 0);
477}472}
478473
479test "getppid" {474test "getppid" {
lib/std/process/Args.zig+1-1
...@@ -6,7 +6,7 @@ const native_os = builtin.os.tag;...@@ -6,7 +6,7 @@ const native_os = builtin.os.tag;
6const std = @import("../std.zig");6const std = @import("../std.zig");
7const Allocator = std.mem.Allocator;7const Allocator = std.mem.Allocator;
8const assert = std.debug.assert;8const assert = std.debug.assert;
9const testing = std.debug.testing;9const testing = std.testing;
1010
11vector: Vector,11vector: Vector,
1212
lib/std/process/Environ.zig+218-182
...@@ -6,15 +6,25 @@ const native_os = builtin.os.tag;...@@ -6,15 +6,25 @@ const native_os = builtin.os.tag;
6const std = @import("../std.zig");6const std = @import("../std.zig");
7const Allocator = std.mem.Allocator;7const Allocator = std.mem.Allocator;
8const assert = std.debug.assert;8const assert = std.debug.assert;
9const testing = std.debug.testing;9const testing = std.testing;
10const unicode = std.unicode;10const unicode = std.unicode;
11const posix = std.posix;11const posix = std.posix;
12const mem = std.mem;12const mem = std.mem;
1313
14/// Unmodified, unprocessed data provided by the operating system.
15///
16/// On Windows this might point to memory in the PEB.
17///
18/// On WASI without libc, this is void because the environment has to be
19/// queried and heap-allocated at runtime.
14block: Block,20block: Block,
1521
16pub const Block = switch (native_os) {22pub const Block = switch (native_os) {
17 .windows => []const u16,23 .windows => []const u16,
24 .wasi => switch (builtin.link_libc) {
25 false => void,
26 true => [:null]const ?[*:0]const u8,
27 },
18 else => [:null]const ?[*:0]const u8,28 else => [:null]const ?[*:0]const u8,
19};29};
2030
...@@ -89,11 +99,11 @@ pub const Map = struct {...@@ -89,11 +99,11 @@ pub const Map = struct {
89 self.* = undefined;99 self.* = undefined;
90 }100 }
91101
92 pub fn keys(m: *Map) [][]const u8 {102 pub fn keys(m: *const Map) [][]const u8 {
93 return m.array_hash_map.keys();103 return m.array_hash_map.keys();
94 }104 }
95105
96 pub fn values(m: *Map) [][]const u8 {106 pub fn values(m: *const Map) [][]const u8 {
97 return m.array_hash_map.values();107 return m.array_hash_map.values();
98 }108 }
99109
...@@ -214,10 +224,10 @@ pub const Map = struct {...@@ -214,10 +224,10 @@ pub const Map = struct {
214224
215 /// Creates a null-delimited environment variable block in the format225 /// Creates a null-delimited environment variable block in the format
216 /// expected by POSIX, from a hash map plus options.226 /// expected by POSIX, from a hash map plus options.
217 pub fn createBlock(227 pub fn createBlockPosix(
218 map: *const Map,228 map: *const Map,
219 arena: Allocator,229 arena: Allocator,
220 options: CreateBlockOptions,230 options: CreateBlockPosixOptions,
221 ) Allocator.Error![:null]?[*:0]u8 {231 ) Allocator.Error![:null]?[*:0]u8 {
222 const ZigProgressAction = enum { nothing, edit, delete, add };232 const ZigProgressAction = enum { nothing, edit, delete, add };
223 const zig_progress_action: ZigProgressAction = a: {233 const zig_progress_action: ZigProgressAction = a: {
...@@ -273,6 +283,46 @@ pub const Map = struct {...@@ -273,6 +283,46 @@ pub const Map = struct {
273 assert(i == envp_count);283 assert(i == envp_count);
274 return envp_buf;284 return envp_buf;
275 }285 }
286
287 /// Caller must free result.
288 pub fn createBlockWindows(map: *const Map, gpa: Allocator) Allocator.Error![]u16 {
289 // count bytes needed
290 const max_chars_needed = x: {
291 // Only need 2 trailing NUL code units for an empty environment
292 var max_chars_needed: usize = if (map.count() == 0) 2 else 1;
293 var it = map.iterator();
294 while (it.next()) |pair| {
295 // +1 for '='
296 // +1 for null byte
297 max_chars_needed += pair.key_ptr.len + pair.value_ptr.len + 2;
298 }
299 break :x max_chars_needed;
300 };
301 const result = try gpa.alloc(u16, max_chars_needed);
302 errdefer gpa.free(result);
303
304 var it = map.iterator();
305 var i: usize = 0;
306 while (it.next()) |pair| {
307 i += try unicode.wtf8ToWtf16Le(result[i..], pair.key_ptr.*);
308 result[i] = '=';
309 i += 1;
310 i += try unicode.wtf8ToWtf16Le(result[i..], pair.value_ptr.*);
311 result[i] = 0;
312 i += 1;
313 }
314 result[i] = 0;
315 i += 1;
316 // An empty environment is a special case that requires a redundant
317 // NUL terminator. CreateProcess will read the second code unit even
318 // though theoretically the first should be enough to recognize that the
319 // environment is empty (see https://nullprogram.com/blog/2023/08/23/)
320 if (map.count() == 0) {
321 result[i] = 0;
322 i += 1;
323 }
324 return try gpa.realloc(result, i);
325 }
276};326};
277327
278pub const CreateMapError = error{328pub const CreateMapError = error{
...@@ -380,162 +430,131 @@ pub fn createMap(env: Environ, allocator: Allocator) CreateMapError!Map {...@@ -380,162 +430,131 @@ pub fn createMap(env: Environ, allocator: Allocator) CreateMapError!Map {
380 }430 }
381}431}
382432
383test createMap {433pub const ContainsError = error{
384 var env = try createMap(testing.allocator);
385 defer env.deinit();
386}
387
388pub const GetEnvVarOwnedError = error{
389 OutOfMemory,434 OutOfMemory,
390 EnvironmentVariableNotFound,435 /// On Windows, environment variable keys provided by the user must be
391436 /// valid [WTF-8](https://wtf-8.codeberg.page/). This error is unreachable
392 /// On Windows, environment variable keys provided by the user must be valid WTF-8.437 /// if the key is statically known to be valid.
393 /// https://wtf-8.codeberg.page/
394 InvalidWtf8,438 InvalidWtf8,
439 /// WASI-only. `environ_sizes_get` or `environ_get` failed for an
440 /// unexpected reason.
441 Unexpected,
395};442};
396443
397/// Caller must free returned memory.
398/// On Windows, if `key` is not valid [WTF-8](https://wtf-8.codeberg.page/),444/// On Windows, if `key` is not valid [WTF-8](https://wtf-8.codeberg.page/),
399/// then `error.InvalidWtf8` is returned.445/// then `error.InvalidWtf8` is returned.
400/// On Windows, the value is encoded as [WTF-8](https://wtf-8.codeberg.page/).446///
401/// On other platforms, the value is an opaque sequence of bytes with no particular encoding.447/// See also:
402pub fn getEnvVarOwned(allocator: Allocator, key: []const u8) GetEnvVarOwnedError![]u8 {448/// * `createMap`
403 if (native_os == .windows) {449/// * `containsConstant`
404 const result_w = blk: {450/// * `containsUnempty`
405 var stack_alloc = std.heap.stackFallback(256 * @sizeOf(u16), allocator);451pub fn contains(environ: Environ, gpa: Allocator, key: []const u8) ContainsError!bool {
406 const stack_allocator = stack_alloc.get();452 var map = try createMap(environ, gpa);
407 const key_w = try unicode.wtf8ToWtf16LeAllocZ(stack_allocator, key);453 defer map.deinit();
408 defer stack_allocator.free(key_w);454 return map.contains(key);
455}
409456
410 break :blk getenvW(key_w) orelse return error.EnvironmentVariableNotFound;457/// On Windows, if `key` is not valid [WTF-8](https://wtf-8.codeberg.page/),
411 };458/// then `error.InvalidWtf8` is returned.
412 // wtf16LeToWtf8Alloc can only fail with OutOfMemory459///
413 return unicode.wtf16LeToWtf8Alloc(allocator, result_w);460/// See also:
414 } else if (native_os == .wasi and !builtin.link_libc) {461/// * `createMap`
415 var envmap = createMap(allocator) catch return error.OutOfMemory;462/// * `containsUnemptyConstant`
416 defer envmap.deinit();463/// * `contains`
417 const val = envmap.get(key) orelse return error.EnvironmentVariableNotFound;464pub fn containsUnempty(environ: Environ, gpa: Allocator, key: []const u8) ContainsError!bool {
418 return allocator.dupe(u8, val);465 var map = try createMap(environ, gpa);
419 } else {466 defer map.deinit();
420 const result = posix.getenv(key) orelse return error.EnvironmentVariableNotFound;467 const value = map.get(key) orelse return false;
421 return allocator.dupe(u8, result);468 return value.len != 0;
422 }
423}469}
424470
425/// On Windows, `key` must be valid WTF-8.471/// This function is unavailable on WASI without libc due to the memory
426pub inline fn hasEnvVarConstant(comptime key: []const u8) bool {472/// allocation requirement.
473///
474/// On Windows, `key` must be valid [WTF-8](https://wtf-8.codeberg.page/),
475///
476/// See also:
477/// * `contains`
478/// * `containsUnemptyConstant`
479/// * `createMap`
480pub inline fn containsConstant(environ: Environ, comptime key: []const u8) bool {
427 if (native_os == .windows) {481 if (native_os == .windows) {
428 const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key);482 const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key);
429 return getenvW(key_w) != null;483 return getWindows(environ, key_w) != null;
430 } else if (native_os == .wasi and !builtin.link_libc) {
431 return false;
432 } else {484 } else {
433 return posix.getenv(key) != null;485 return getPosix(environ, key) != null;
434 }486 }
435}487}
436488
437/// On Windows, `key` must be valid WTF-8.489/// This function is unavailable on WASI without libc due to the memory
438pub inline fn hasNonEmptyEnvVarConstant(comptime key: []const u8) bool {490/// allocation requirement.
491///
492/// On Windows, `key` must be valid [WTF-8](https://wtf-8.codeberg.page/),
493///
494/// See also:
495/// * `containsUnempty`
496/// * `containsConstant`
497/// * `createMap`
498pub inline fn containsUnemptyConstant(environ: Environ, comptime key: []const u8) bool {
439 if (native_os == .windows) {499 if (native_os == .windows) {
440 const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key);500 const key_w = comptime unicode.wtf8ToWtf16LeStringLiteral(key);
441 const value = getenvW(key_w) orelse return false;501 const value = getWindows(environ, key_w) orelse return false;
442 return value.len != 0;502 return value.len != 0;
443 } else if (native_os == .wasi and !builtin.link_libc) {
444 return false;
445 } else {503 } else {
446 const value = posix.getenv(key) orelse return false;504 const value = getPosix(environ, key) orelse return false;
447 return value.len != 0;505 return value.len != 0;
448 }506 }
449}507}
450508
451pub const ParseIntError = std.fmt.ParseIntError || error{EnvironmentVariableNotFound};509/// This function is unavailable on WASI without libc due to the memory
452510/// allocation requirement.
453/// Parses an environment variable as an integer.
454///511///
455/// On Windows, `key` must be valid WTF-8.512/// See also:
456pub fn parseInt(io: std.Io, key: []const u8, comptime I: type, base: u8) ParseIntError!I {513/// * `getWindows`
457 const text = io.environ(key) orelse return error.EnvironmentVariableNotFound;514/// * `createMap`
458 return std.fmt.parseInt(I, text, base);515pub fn getPosix(environ: Environ, key: []const u8) ?[:0]const u8 {
459}516 if (mem.findScalar(u8, key, '=') != null) return null;
460517 for (environ.block) |opt_line| {
461pub const HasEnvVarError = error{518 const line = opt_line.?;
462 OutOfMemory,519 var line_i: usize = 0;
463520 while (line[line_i] != 0) : (line_i += 1) {
464 /// On Windows, environment variable keys provided by the user must be valid WTF-8.521 if (line_i == key.len) break;
465 /// https://wtf-8.codeberg.page/522 if (line[line_i] != key[line_i]) break;
466 InvalidWtf8,523 }
467};524 if ((line_i != key.len) or (line[line_i] != '=')) continue;
468
469/// On Windows, if `key` is not valid [WTF-8](https://wtf-8.codeberg.page/),
470/// then `error.InvalidWtf8` is returned.
471pub fn hasEnvVar(allocator: Allocator, key: []const u8) HasEnvVarError!bool {
472 if (native_os == .windows) {
473 var stack_alloc = std.heap.stackFallback(256 * @sizeOf(u16), allocator);
474 const stack_allocator = stack_alloc.get();
475 const key_w = try unicode.wtf8ToWtf16LeAllocZ(stack_allocator, key);
476 defer stack_allocator.free(key_w);
477 return getenvW(key_w) != null;
478 } else if (native_os == .wasi and !builtin.link_libc) {
479 var envmap = createMap(allocator) catch return error.OutOfMemory;
480 defer envmap.deinit();
481 return envmap.getPtr(key) != null;
482 } else {
483 return posix.getenv(key) != null;
484 }
485}
486525
487/// On Windows, if `key` is not valid [WTF-8](https://wtf-8.codeberg.page/),526 return mem.sliceTo(line + line_i + 1, 0);
488/// then `error.InvalidWtf8` is returned.
489pub fn hasNonEmptyEnvVar(allocator: Allocator, key: []const u8) HasEnvVarError!bool {
490 if (native_os == .windows) {
491 var stack_alloc = std.heap.stackFallback(256 * @sizeOf(u16), allocator);
492 const stack_allocator = stack_alloc.get();
493 const key_w = try unicode.wtf8ToWtf16LeAllocZ(stack_allocator, key);
494 defer stack_allocator.free(key_w);
495 const value = getenvW(key_w) orelse return false;
496 return value.len != 0;
497 } else if (native_os == .wasi and !builtin.link_libc) {
498 var envmap = createMap(allocator) catch return error.OutOfMemory;
499 defer envmap.deinit();
500 const value = envmap.getPtr(key) orelse return false;
501 return value.len != 0;
502 } else {
503 const value = posix.getenv(key) orelse return false;
504 return value.len != 0;
505 }527 }
528 return null;
506}529}
507530
508/// Windows-only. Get an environment variable with a null-terminated, WTF-16 encoded name.531/// Windows-only. Get an environment variable with a null-terminated, WTF-16
509/// The returned slice points to memory in the PEB.532/// encoded name.
510///533///
511/// This function performs a Unicode-aware case-insensitive lookup using RtlEqualUnicodeString.534/// This function performs a Unicode-aware case-insensitive lookup using
535/// RtlEqualUnicodeString.
512///536///
513/// See also:537/// See also:
514/// * `std.posix.getenv`
515/// * `createMap`538/// * `createMap`
516/// * `getEnvVarOwned`539/// * `containsConstant`
517/// * `hasEnvVarConstant`540/// * `contains`
518/// * `hasEnvVar`541pub fn getWindows(environ: Environ, key: [*:0]const u16) ?[:0]const u16 {
519pub fn getenvW(key: [*:0]const u16) ?[:0]const u16 {542 comptime assert(native_os == .windows);
520 if (native_os != .windows) {543
521 @compileError("Windows-only");544 // '=' anywhere but the start makes this an invalid environment variable name.
522 }
523 const key_slice = mem.sliceTo(key, 0);545 const key_slice = mem.sliceTo(key, 0);
524 // '=' anywhere but the start makes this an invalid environment variable name546 if (key_slice.len > 0 and mem.findScalar(u16, key_slice[1..], '=') != null) return null;
525 if (key_slice.len > 0 and std.mem.findScalar(u16, key_slice[1..], '=') != null) {547
526 return null;
527 }
528 const ptr = std.os.windows.peb().ProcessParameters.Environment;
529 var i: usize = 0;548 var i: usize = 0;
530 while (ptr[i] != 0) {549 while (environ.block[i] != 0) {
531 const key_value = mem.sliceTo(ptr[i..], 0);550 const key_value = mem.sliceTo(environ.block[i..], 0);
532551
533 // There are some special environment variables that start with =,552 // There are some special environment variables that start with =,
534 // so we need a special case to not treat = as a key/value separator553 // so we need a special case to not treat = as a key/value separator
535 // if it's the first character.554 // if it's the first character.
536 // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133555 // https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133
537 const equal_search_start: usize = if (key_value[0] == '=') 1 else 0;556 const equal_search_start: usize = if (key_value[0] == '=') 1 else 0;
538 const equal_index = std.mem.findScalarPos(u16, key_value, equal_search_start, '=') orelse {557 const equal_index = mem.findScalarPos(u16, key_value, equal_search_start, '=') orelse {
539 // This is enforced by CreateProcess.558 // This is enforced by CreateProcess.
540 // If violated, CreateProcess will fail with INVALID_PARAMETER.559 // If violated, CreateProcess will fail with INVALID_PARAMETER.
541 unreachable; // must contain a =560 unreachable; // must contain a =
...@@ -552,25 +571,35 @@ pub fn getenvW(key: [*:0]const u16) ?[:0]const u16 {...@@ -552,25 +571,35 @@ pub fn getenvW(key: [*:0]const u16) ?[:0]const u16 {
552 return null;571 return null;
553}572}
554573
555test getEnvVarOwned {574pub const GetAllocError = error{
556 try testing.expectError(575 OutOfMemory,
557 error.EnvironmentVariableNotFound,576 EnvironmentVariableMissing,
558 getEnvVarOwned(std.testing.allocator, "BADENV"),577 /// On Windows, environment variable keys provided by the user must be
559 );578 /// valid [WTF-8](https://wtf-8.codeberg.page/). This error is unreachable
560}579 /// if the key is statically known to be valid.
561580 InvalidWtf8,
562test hasEnvVarConstant {581};
563 if (native_os == .wasi and !builtin.link_libc) return error.SkipZigTest;
564
565 try testing.expect(!hasEnvVarConstant("BADENV"));
566}
567582
568test hasEnvVar {583/// Caller owns returned memory.
569 const has_env = try hasEnvVar(std.testing.allocator, "BADENV");584///
570 try testing.expect(!has_env);585/// On Windows:
586/// * If `key` is not valid [WTF-8](https://wtf-8.codeberg.page/), then
587/// `error.InvalidWtf8` is returned.
588/// * The returned value is encoded as [WTF-8](https://wtf-8.codeberg.page/).
589///
590/// On other platforms, the value is an opaque sequence of bytes with no
591/// particular encoding.
592///
593/// See also:
594/// * `createMap`
595pub fn getAlloc(environ: Environ, gpa: Allocator, key: []const u8) GetAllocError![]u8 {
596 var map = createMap(environ, gpa) catch return error.OutOfMemory;
597 defer map.deinit();
598 const val = map.get(key) orelse return error.EnvironmentVariableMissing;
599 return gpa.dupe(u8, val);
571}600}
572601
573pub const CreateBlockOptions = struct {602pub const CreateBlockPosixOptions = struct {
574 /// `null` means to leave the `ZIG_PROGRESS` environment variable unmodified.603 /// `null` means to leave the `ZIG_PROGRESS` environment variable unmodified.
575 /// If non-null, negative means to remove the environment variable, and >= 0604 /// If non-null, negative means to remove the environment variable, and >= 0
576 /// means to provide it with the given integer.605 /// means to provide it with the given integer.
...@@ -579,7 +608,11 @@ pub const CreateBlockOptions = struct {...@@ -579,7 +608,11 @@ pub const CreateBlockOptions = struct {
579608
580/// Creates a null-delimited environment variable block in the format expected609/// Creates a null-delimited environment variable block in the format expected
581/// by POSIX, from a different one.610/// by POSIX, from a different one.
582pub fn createBlock(existing: Environ, arena: Allocator, options: CreateBlockOptions) Allocator.Error![:null]?[*:0]u8 {611pub fn createBlockPosix(
612 existing: Environ,
613 arena: Allocator,
614 options: CreateBlockPosixOptions,
615) Allocator.Error![:null]?[*:0]u8 {
583 const contains_zig_progress = for (existing.block) |opt_line| {616 const contains_zig_progress = for (existing.block) |opt_line| {
584 if (mem.eql(u8, mem.sliceTo(opt_line.?, '='), "ZIG_PROGRESS")) break true;617 if (mem.eql(u8, mem.sliceTo(opt_line.?, '='), "ZIG_PROGRESS")) break true;
585 } else false;618 } else false;
...@@ -646,7 +679,7 @@ test "Map.createBlock" {...@@ -646,7 +679,7 @@ test "Map.createBlock" {
646679
647 var arena = std.heap.ArenaAllocator.init(allocator);680 var arena = std.heap.ArenaAllocator.init(allocator);
648 defer arena.deinit();681 defer arena.deinit();
649 const environ = try envmap.createBlock(arena.allocator(), .{});682 const environ = try envmap.createBlockPosix(arena.allocator(), .{});
650683
651 try testing.expectEqual(@as(usize, 5), environ.len);684 try testing.expectEqual(@as(usize, 5), environ.len);
652685
...@@ -665,46 +698,6 @@ test "Map.createBlock" {...@@ -665,46 +698,6 @@ test "Map.createBlock" {
665 }698 }
666}699}
667700
668/// Caller must free result.
669pub fn createWindowsEnvBlock(allocator: mem.Allocator, env_map: *const Map) ![]u16 {
670 // count bytes needed
671 const max_chars_needed = x: {
672 // Only need 2 trailing NUL code units for an empty environment
673 var max_chars_needed: usize = if (env_map.count() == 0) 2 else 1;
674 var it = env_map.iterator();
675 while (it.next()) |pair| {
676 // +1 for '='
677 // +1 for null byte
678 max_chars_needed += pair.key_ptr.len + pair.value_ptr.len + 2;
679 }
680 break :x max_chars_needed;
681 };
682 const result = try allocator.alloc(u16, max_chars_needed);
683 errdefer allocator.free(result);
684
685 var it = env_map.iterator();
686 var i: usize = 0;
687 while (it.next()) |pair| {
688 i += try unicode.wtf8ToWtf16Le(result[i..], pair.key_ptr.*);
689 result[i] = '=';
690 i += 1;
691 i += try unicode.wtf8ToWtf16Le(result[i..], pair.value_ptr.*);
692 result[i] = 0;
693 i += 1;
694 }
695 result[i] = 0;
696 i += 1;
697 // An empty environment is a special case that requires a redundant
698 // NUL terminator. CreateProcess will read the second code unit even
699 // though theoretically the first should be enough to recognize that the
700 // environment is empty (see https://nullprogram.com/blog/2023/08/23/)
701 if (env_map.count() == 0) {
702 result[i] = 0;
703 i += 1;
704 }
705 return try allocator.realloc(result, i);
706}
707
708test Map {701test Map {
709 var env = Map.init(testing.allocator);702 var env = Map.init(testing.allocator);
710 defer env.deinit();703 defer env.deinit();
...@@ -733,13 +726,14 @@ test Map {...@@ -733,13 +726,14 @@ test Map {
733 var it = env.iterator();726 var it = env.iterator();
734 var count: Map.Size = 0;727 var count: Map.Size = 0;
735 while (it.next()) |entry| {728 while (it.next()) |entry| {
736 const is_an_expected_name = std.mem.eql(u8, "SOMETHING_NEW", entry.key_ptr.*) or std.mem.eql(u8, "SOMETHING_NEW_AND_LONGER", entry.key_ptr.*);729 const is_an_expected_name = mem.eql(u8, "SOMETHING_NEW", entry.key_ptr.*) or mem.eql(u8, "SOMETHING_NEW_AND_LONGER", entry.key_ptr.*);
737 try testing.expect(is_an_expected_name);730 try testing.expect(is_an_expected_name);
738 count += 1;731 count += 1;
739 }732 }
740 try testing.expectEqual(@as(Map.Size, 2), count);733 try testing.expectEqual(@as(Map.Size, 2), count);
741734
742 env.remove("SOMETHING_NEW");735 try testing.expect(env.swapRemove("SOMETHING_NEW"));
736 try testing.expect(!env.swapRemove("SOMETHING_NEW"));
743 try testing.expect(env.get("SOMETHING_NEW") == null);737 try testing.expect(env.get("SOMETHING_NEW") == null);
744738
745 try testing.expectEqual(@as(Map.Size, 1), env.count());739 try testing.expectEqual(@as(Map.Size, 1), env.count());
...@@ -751,7 +745,7 @@ test Map {...@@ -751,7 +745,7 @@ test Map {
751745
752 // and WTF-8 that's not valid UTF-8746 // and WTF-8 that's not valid UTF-8
753 const wtf8_with_surrogate_pair = try unicode.wtf16LeToWtf8Alloc(testing.allocator, &[_]u16{747 const wtf8_with_surrogate_pair = try unicode.wtf16LeToWtf8Alloc(testing.allocator, &[_]u16{
754 std.mem.nativeToLittle(u16, 0xD83D), // unpaired high surrogate748 mem.nativeToLittle(u16, 0xD83D), // unpaired high surrogate
755 });749 });
756 defer testing.allocator.free(wtf8_with_surrogate_pair);750 defer testing.allocator.free(wtf8_with_surrogate_pair);
757751
...@@ -759,3 +753,45 @@ test Map {...@@ -759,3 +753,45 @@ test Map {
759 try testing.expectEqualSlices(u8, wtf8_with_surrogate_pair, env.get(wtf8_with_surrogate_pair).?);753 try testing.expectEqualSlices(u8, wtf8_with_surrogate_pair, env.get(wtf8_with_surrogate_pair).?);
760 }754 }
761}755}
756
757test "convert from Environ to Map and back again" {
758 const gpa = testing.allocator;
759
760 var map: Map = .init(gpa);
761 defer map.deinit();
762 try map.put("FOO", "BAR");
763 try map.put("A", "");
764 try map.put("", "B");
765
766 var arena_allocator = std.heap.ArenaAllocator.init(gpa);
767 defer arena_allocator.deinit();
768 const arena = arena_allocator.allocator();
769
770 const environ: Environ = switch (native_os) {
771 .windows => .{ .block = try map.createBlockWindows(arena) },
772 .wasi => if (!builtin.libc) return error.SkipZigTest,
773 else => .{ .block = try map.createBlockPosix(arena, .{}) },
774 };
775
776 try testing.expectEqual(true, environ.contains(gpa, "FOO"));
777 try testing.expectEqual(false, environ.contains(gpa, "BAR"));
778 try testing.expectEqual(true, environ.contains(gpa, "A"));
779 try testing.expectEqual(true, environ.containsConstant("A"));
780 try testing.expectEqual(false, environ.containsUnempty(gpa, "A"));
781 try testing.expectEqual(false, environ.containsUnemptyConstant("A"));
782 try testing.expectEqual(true, environ.contains(gpa, ""));
783 try testing.expectEqual(false, environ.contains(gpa, "B"));
784
785 try testing.expectError(error.EnvironmentVariableMissing, environ.getAlloc(gpa, "BOGUS"));
786 {
787 const value = try environ.getAlloc(gpa, "FOO");
788 defer gpa.free(value);
789 try testing.expectEqualStrings("BAR", value);
790 }
791
792 var map2 = try environ.createMap(gpa);
793 defer map2.deinit();
794
795 try testing.expectEqualSlices([]const u8, map.keys(), map2.keys());
796 try testing.expectEqualSlices([]const u8, map.values(), map2.values());
797}
lib/std/zig.zig+2
...@@ -739,6 +739,8 @@ pub const EnvVar = enum {...@@ -739,6 +739,8 @@ pub const EnvVar = enum {
739 ZIG_VERBOSE_CC,739 ZIG_VERBOSE_CC,
740 ZIG_BTRFS_WORKAROUND,740 ZIG_BTRFS_WORKAROUND,
741 ZIG_DEBUG_CMD,741 ZIG_DEBUG_CMD,
742 ZIG_IS_DETECTING_LIBC_PATHS,
743 ZIG_IS_TRYING_TO_NOT_CALL_ITSELF,
742 CC,744 CC,
743 NO_COLOR,745 NO_COLOR,
744 CLICOLOR_FORCE,746 CLICOLOR_FORCE,
src/main.zig+23-23
...@@ -168,7 +168,7 @@ const use_debug_allocator = build_options.debug_gpa or...@@ -168,7 +168,7 @@ const use_debug_allocator = build_options.debug_gpa or
168 .ReleaseFast, .ReleaseSmall => false,168 .ReleaseFast, .ReleaseSmall => false,
169 });169 });
170170
171pub fn main() anyerror!void {171pub fn main(init: std.process.Init.Minimal) anyerror!void {
172 const gpa = gpa: {172 const gpa = gpa: {
173 if (use_debug_allocator) break :gpa debug_allocator.allocator();173 if (use_debug_allocator) break :gpa debug_allocator.allocator();
174 if (native_os == .wasi) break :gpa std.heap.wasm_allocator;174 if (native_os == .wasi) break :gpa std.heap.wasm_allocator;
...@@ -182,26 +182,25 @@ pub fn main() anyerror!void {...@@ -182,26 +182,25 @@ pub fn main() anyerror!void {
182 defer arena_instance.deinit();182 defer arena_instance.deinit();
183 const arena = arena_instance.allocator();183 const arena = arena_instance.allocator();
184184
185 const args = try process.argsAlloc(arena);185 const args = try init.args.toSlice(arena);
186186
187 if (args.len > 0) crash_report.zig_argv0 = args[0];187 if (args.len > 0) crash_report.zig_argv0 = args[0];
188188
189 var env_map = init.environ.createMap(arena) catch |err| fatal("failed to parse environment: {t}", .{err});
190
189 if (tracy.enable_allocation) {191 if (tracy.enable_allocation) {
190 var gpa_tracy = tracy.tracyAllocator(gpa);192 var gpa_tracy = tracy.tracyAllocator(gpa);
191 return mainArgs(gpa_tracy.allocator(), arena, args);193 return mainArgs(gpa_tracy.allocator(), arena, args, &env_map);
192 }194 }
193195
194 if (native_os == .wasi) {196 if (native_os == .wasi) {
195 wasi_preopens = try fs.wasi.preopensAlloc(arena);197 wasi_preopens = try fs.wasi.preopensAlloc(arena);
196 }198 }
197199
198 return mainArgs(gpa, arena, args);200 return mainArgs(gpa, arena, args, &env_map);
199}201}
200202
201fn mainArgs(gpa: Allocator, arena: Allocator, args: []const [:0]const u8) !void {203fn mainArgs(gpa: Allocator, arena: Allocator, args: []const [:0]const u8, env_map: *process.Environ.Map) !void {
202 const tr = tracy.trace(@src());
203 defer tr.end();
204
205 Compilation.setMainThread();204 Compilation.setMainThread();
206205
207 if (args.len <= 1) {206 if (args.len <= 1) {
...@@ -209,7 +208,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const [:0]const u8) !void...@@ -209,7 +208,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const [:0]const u8) !void
209 fatal("expected command argument", .{});208 fatal("expected command argument", .{});
210 }209 }
211210
212 if (process.can_execv and std.posix.getenvZ("ZIG_IS_DETECTING_LIBC_PATHS") != null) {211 if (process.can_replace and std.zig.EnvVar.ZIG_IS_DETECTING_LIBC_PATHS.isSet(env_map)) {
213 dev.check(.cc_command);212 dev.check(.cc_command);
214 // In this case we have accidentally invoked ourselves as "the system C compiler"213 // In this case we have accidentally invoked ourselves as "the system C compiler"
215 // to figure out where libc is installed. This is essentially infinite recursion214 // to figure out where libc is installed. This is essentially infinite recursion
...@@ -217,27 +216,28 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const [:0]const u8) !void...@@ -217,27 +216,28 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const [:0]const u8) !void
217 // Here we ignore the CC environment variable and exec `cc` as a child process.216 // Here we ignore the CC environment variable and exec `cc` as a child process.
218 // However it's possible Zig is installed as *that* C compiler as well, which is217 // However it's possible Zig is installed as *that* C compiler as well, which is
219 // why we have this additional environment variable here to check.218 // why we have this additional environment variable here to check.
220 var env_map = try process.getEnvMap(arena);219
221220 const inf_loop_env_key: std.zig.EnvVar = .ZIG_IS_TRYING_TO_NOT_CALL_ITSELF;
222 const inf_loop_env_key = "ZIG_IS_TRYING_TO_NOT_CALL_ITSELF";221 if (inf_loop_env_key.isSet(env_map)) {
223 if (env_map.get(inf_loop_env_key) != null) {222 fatal("{s}", .{
224 fatal("The compilation links against libc, but Zig is unable to provide a libc " ++223 "The compilation links against libc, but Zig is unable to provide a libc " ++
225 "for this operating system, and no --libc " ++224 "for this operating system, and no --libc " ++
226 "parameter was provided, so Zig attempted to invoke the system C compiler " ++225 "parameter was provided, so Zig attempted to invoke the system C compiler " ++
227 "in order to determine where libc is installed. However the system C " ++226 "in order to determine where libc is installed. However the system C " ++
228 "compiler is `zig cc`, so no libc installation was found.", .{});227 "compiler is `zig cc`, so no libc installation was found.",
228 });
229 }229 }
230 try env_map.put(inf_loop_env_key, "1");230 try env_map.put(@tagName(inf_loop_env_key), "1");
231231
232 // Some programs such as CMake will strip the `cc` and subsequent args from the232 // Some programs such as CMake will strip the `cc` and subsequent args from the
233 // CC environment variable. We detect and support this scenario here because of233 // CC environment variable. We detect and support this scenario here because of
234 // the ZIG_IS_DETECTING_LIBC_PATHS environment variable.234 // the ZIG_IS_DETECTING_LIBC_PATHS environment variable.
235 if (mem.eql(u8, args[1], "cc")) {235 if (mem.eql(u8, args[1], "cc")) {
236 return process.execve(arena, args[1..], &env_map);236 return process.replace(.{ .argv = args[1..], .env_map = env_map });
237 } else {237 } else {
238 const modified_args = try arena.dupe([]const u8, args);238 const modified_args = try arena.dupe([]const u8, args);
239 modified_args[0] = "cc";239 modified_args[0] = "cc";
240 return process.execve(arena, modified_args, &env_map);240 return process.replace(.{ .argv = modified_args, .env_map = env_map });
241 }241 }
242 }242 }
243243
...@@ -4431,7 +4431,7 @@ fn runOrTest(...@@ -4431,7 +4431,7 @@ fn runOrTest(
44314431
4432 // We do not execve for tests because if the test fails we want to print4432 // We do not execve for tests because if the test fails we want to print
4433 // the error message and invocation below.4433 // the error message and invocation below.
4434 if (process.can_execv and arg_mode == .run) {4434 if (process.can_replace and arg_mode == .run) {
4435 // execv releases the locks; no need to destroy the Compilation here.4435 // execv releases the locks; no need to destroy the Compilation here.
4436 _ = try io.lockStderr(&.{}, .no_color);4436 _ = try io.lockStderr(&.{}, .no_color);
4437 const err = process.execve(gpa, argv.items, &env_map);4437 const err = process.execve(gpa, argv.items, &env_map);
...@@ -5668,7 +5668,7 @@ fn jitCmd(...@@ -5668,7 +5668,7 @@ fn jitCmd(
56685668
5669 child_argv.appendSliceAssumeCapacity(args);5669 child_argv.appendSliceAssumeCapacity(args);
56705670
5671 if (process.can_execv and options.capture == null) {5671 if (process.can_replace and options.capture == null) {
5672 if (EnvVar.ZIG_DEBUG_CMD.isSet()) {5672 if (EnvVar.ZIG_DEBUG_CMD.isSet()) {
5673 const cmd = try std.mem.join(arena, " ", child_argv.items);5673 const cmd = try std.mem.join(arena, " ", child_argv.items);
5674 std.debug.print("{s}\n", .{cmd});5674 std.debug.print("{s}\n", .{cmd});