Commit 44c14749a1

Andrew Kelley <andrew@ziglang.org>
2020-02-17 06:58:30
expand argv[0] when spawning system C compiler
Some C compilers, such as Clang, are known to rely on argv[0] to find the path to their own executable, without even bothering to resolve PATH. This results in the message: error: unable to execute command: Executable "" doesn't exist! So we tell ChildProcess to expand argv[0] to the absolute path to give them a helping hand.
1 parent a5d47be
Changed files (3)
lib/std/child_process.zig
@@ -49,6 +49,10 @@ pub const ChildProcess = struct {
 
     err_pipe: if (builtin.os == .windows) void else [2]os.fd_t,
 
+    expand_arg0: Arg0Expand,
+
+    pub const Arg0Expand = os.Arg0Expand;
+
     pub const SpawnError = error{
         OutOfMemory,
 
@@ -100,6 +104,7 @@ pub const ChildProcess = struct {
             .stdin_behavior = StdIo.Inherit,
             .stdout_behavior = StdIo.Inherit,
             .stderr_behavior = StdIo.Inherit,
+            .expand_arg0 = .no_expand,
         };
         errdefer allocator.destroy(child);
         return child;
@@ -172,34 +177,56 @@ pub const ChildProcess = struct {
 
     /// Spawns a child process, waits for it, collecting stdout and stderr, and then returns.
     /// If it succeeds, the caller owns result.stdout and result.stderr memory.
+    /// TODO deprecate in favor of exec2
     pub fn exec(
         allocator: *mem.Allocator,
         argv: []const []const u8,
         cwd: ?[]const u8,
         env_map: ?*const BufMap,
-        max_output_size: usize,
+        max_output_bytes: usize,
     ) !ExecResult {
-        const child = try ChildProcess.init(argv, allocator);
+        return exec2(.{
+            .allocator = allocator,
+            .argv = argv,
+            .cwd = cwd,
+            .env_map = env_map,
+            .max_output_bytes = max_output_bytes,
+        });
+    }
+
+    /// Spawns a child process, waits for it, collecting stdout and stderr, and then returns.
+    /// If it succeeds, the caller owns result.stdout and result.stderr memory.
+    /// TODO rename to exec
+    pub fn exec2(args: struct {
+        allocator: *mem.Allocator,
+        argv: []const []const u8,
+        cwd: ?[]const u8 = null,
+        env_map: ?*const BufMap = null,
+        max_output_bytes: usize = 50 * 1024,
+        expand_arg0: Arg0Expand = .no_expand,
+    }) !ExecResult {
+        const child = try ChildProcess.init(args.argv, args.allocator);
         defer child.deinit();
 
-        child.stdin_behavior = ChildProcess.StdIo.Ignore;
-        child.stdout_behavior = ChildProcess.StdIo.Pipe;
-        child.stderr_behavior = ChildProcess.StdIo.Pipe;
-        child.cwd = cwd;
-        child.env_map = env_map;
+        child.stdin_behavior = .Ignore;
+        child.stdout_behavior = .Pipe;
+        child.stderr_behavior = .Pipe;
+        child.cwd = args.cwd;
+        child.env_map = args.env_map;
+        child.expand_arg0 = args.expand_arg0;
 
         try child.spawn();
 
-        var stdout = Buffer.initNull(allocator);
-        var stderr = Buffer.initNull(allocator);
+        var stdout = Buffer.initNull(args.allocator);
+        var stderr = Buffer.initNull(args.allocator);
         defer Buffer.deinit(&stdout);
         defer Buffer.deinit(&stderr);
 
         var stdout_file_in_stream = child.stdout.?.inStream();
         var stderr_file_in_stream = child.stderr.?.inStream();
 
-        try stdout_file_in_stream.stream.readAllBuffer(&stdout, max_output_size);
-        try stderr_file_in_stream.stream.readAllBuffer(&stderr, max_output_size);
+        try stdout_file_in_stream.stream.readAllBuffer(&stdout, args.max_output_bytes);
+        try stderr_file_in_stream.stream.readAllBuffer(&stderr, args.max_output_bytes);
 
         return ExecResult{
             .term = try child.wait(),
@@ -418,7 +445,7 @@ pub const ChildProcess = struct {
                 os.setreuid(uid, uid) catch |err| forkChildErrReport(err_pipe[1], err);
             }
 
-            const err = os.execvpe(self.allocator, self.argv, env_map);
+            const err = os.execvpe_expandArg0(self.allocator, self.expand_arg0, self.argv, env_map);
             forkChildErrReport(err_pipe[1], err);
         }
 
lib/std/os.zig
@@ -916,10 +916,13 @@ pub const ExecveError = error{
     NameTooLong,
 } || UnexpectedError;
 
+/// Deprecated in favor of `execveZ`.
+pub const execveC = execveZ;
+
 /// Like `execve` except the parameters are null-terminated,
 /// matching the syscall API on all targets. This removes the need for an allocator.
-/// This function ignores PATH environment variable. See `execvpeC` for that.
-pub fn execveC(path: [*:0]const u8, child_argv: [*:null]const ?[*:0]const u8, envp: [*:null]const ?[*:0]const u8) ExecveError {
+/// This function ignores PATH environment variable. See `execvpeZ` for that.
+pub fn execveZ(path: [*:0]const u8, child_argv: [*:null]const ?[*:0]const u8, envp: [*:null]const ?[*:0]const u8) ExecveError {
     switch (errno(system.execve(path, child_argv, envp))) {
         0 => unreachable,
         EFAULT => unreachable,
@@ -942,11 +945,25 @@ pub fn execveC(path: [*:0]const u8, child_argv: [*:null]const ?[*:0]const u8, en
     }
 }
 
-/// Like `execvpe` except the parameters are null-terminated,
-/// matching the syscall API on all targets. This removes the need for an allocator.
-/// This function also uses the PATH environment variable to get the full path to the executable.
-/// If `file` is an absolute path, this is the same as `execveC`.
-pub fn execvpeC(file: [*:0]const u8, child_argv: [*:null]const ?[*:0]const u8, envp: [*:null]const ?[*:0]const u8) ExecveError {
+/// Deprecated in favor of `execvpeZ`.
+pub const execvpeC = execvpeZ;
+
+pub const Arg0Expand = enum {
+    expand,
+    no_expand,
+};
+
+/// Like `execvpeZ` except if `arg0_expand` is `.expand`, then `argv` is mutable,
+/// and `argv[0]` is expanded to be the same absolute path that is passed to the execve syscall.
+pub fn execvpeZ_expandArg0(
+    comptime arg0_expand: Arg0Expand,
+    file: [*:0]const u8,
+    child_argv: switch (arg0_expand) {
+        .expand => [*:null]?[*:0]const u8,
+        .no_expand => [*:null]const ?[*:0]const u8,
+    },
+    envp: [*:null]const ?[*:0]const u8,
+) ExecveError {
     const file_slice = mem.toSliceConst(u8, file);
     if (mem.indexOfScalar(u8, file_slice, '/') != null) return execveC(file, child_argv, envp);
 
@@ -962,7 +979,12 @@ pub fn execvpeC(file: [*:0]const u8, child_argv: [*:null]const ?[*:0]const u8, e
         mem.copy(u8, path_buf[search_path.len + 1 ..], file_slice);
         const path_len = search_path.len + file_slice.len + 1;
         path_buf[path_len] = 0;
-        err = execveC(path_buf[0..path_len :0].ptr, child_argv, envp);
+        const full_path = path_buf[0..path_len :0].ptr;
+        switch (arg0_expand) {
+            .expand => child_argv[0] = full_path,
+            .no_expand => {},
+        }
+        err = execveC(full_path, child_argv, envp);
         switch (err) {
             error.AccessDenied => seen_eacces = true,
             error.FileNotFound, error.NotDir => {},
@@ -973,13 +995,24 @@ pub fn execvpeC(file: [*:0]const u8, child_argv: [*:null]const ?[*:0]const u8, e
     return err;
 }
 
-/// This function must allocate memory to add a null terminating bytes on path and each arg.
-/// It must also convert to KEY=VALUE\0 format for environment variables, and include null
-/// pointers after the args and after the environment variables.
-/// `argv_slice[0]` is the executable path.
+/// Like `execvpe` except the parameters are null-terminated,
+/// matching the syscall API on all targets. This removes the need for an allocator.
 /// This function also uses the PATH environment variable to get the full path to the executable.
-pub fn execvpe(
+/// If `file` is an absolute path, this is the same as `execveC`.
+pub fn execvpeZ(
+    file: [*:0]const u8,
+    argv: [*:null]const ?[*:0]const u8,
+    envp: [*:null]const ?[*:0]const u8,
+) ExecveError {
+    return execvpeZ_expandArg0(.no_expand, file, argv, envp);
+}
+
+/// This is the same as `execvpe` except if the `arg0_expand` parameter is set to `.expand`,
+/// then argv[0] will be replaced with the expanded version of it, after resolving in accordance
+/// with the PATH environment variable.
+pub fn execvpe_expandArg0(
     allocator: *mem.Allocator,
+    arg0_expand: Arg0Expand,
     argv_slice: []const []const u8,
     env_map: *const std.BufMap,
 ) (ExecveError || error{OutOfMemory}) {
@@ -1004,7 +1037,23 @@ pub fn execvpe(
     const envp_buf = try createNullDelimitedEnvMap(allocator, env_map);
     defer freeNullDelimitedEnvMap(allocator, envp_buf);
 
-    return execvpeC(argv_buf.ptr[0].?, argv_ptr, envp_buf.ptr);
+    switch (arg0_expand) {
+        .expand => return execvpeZ_expandArg0(.expand, argv_buf.ptr[0].?, argv_ptr, envp_buf.ptr),
+        .no_expand => return execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_ptr, envp_buf.ptr),
+    }
+}
+
+/// This function must allocate memory to add a null terminating bytes on path and each arg.
+/// It must also convert to KEY=VALUE\0 format for environment variables, and include null
+/// pointers after the args and after the environment variables.
+/// `argv_slice[0]` is the executable path.
+/// This function also uses the PATH environment variable to get the full path to the executable.
+pub fn execvpe(
+    allocator: *mem.Allocator,
+    argv_slice: []const []const u8,
+    env_map: *const std.BufMap,
+) (ExecveError || error{OutOfMemory}) {
+    return execvpe_expandArg0(allocator, .no_expand, argv_slice, env_map);
 }
 
 pub fn createNullDelimitedEnvMap(allocator: *mem.Allocator, env_map: *const std.BufMap) ![:null]?[*:0]u8 {
src-self-hosted/libc_installation.zig
@@ -241,8 +241,16 @@ pub const LibCInstallation = struct {
             "-xc",
             dev_null,
         };
-        const max_bytes = 1024 * 1024;
-        const exec_res = std.ChildProcess.exec(allocator, &argv, null, null, max_bytes) catch |err| switch (err) {
+        const exec_res = std.ChildProcess.exec2(.{
+            .allocator = allocator,
+            .argv = &argv,
+            .max_output_bytes = 1024 * 1024,
+            // Some C compilers, such as Clang, are known to rely on argv[0] to find the path
+            // to their own executable, without even bothering to resolve PATH. This results in the message:
+            // error: unable to execute command: Executable "" doesn't exist!
+            // So we use the expandArg0 variant of ChildProcess to give them a helping hand.
+            .expand_arg0 = .expand,
+        }) catch |err| switch (err) {
             error.OutOfMemory => return error.OutOfMemory,
             else => return error.UnableToSpawnCCompiler,
         };
@@ -494,8 +502,16 @@ pub fn ccPrintFileName(
     defer allocator.free(arg1);
     const argv = [_][]const u8{ cc_exe, arg1 };
 
-    const max_bytes = 1024 * 1024;
-    const exec_res = std.ChildProcess.exec(allocator, &argv, null, null, max_bytes) catch |err| switch (err) {
+    const exec_res = std.ChildProcess.exec2(.{
+        .allocator = allocator,
+        .argv = &argv,
+        .max_output_bytes = 1024 * 1024,
+        // Some C compilers, such as Clang, are known to rely on argv[0] to find the path
+        // to their own executable, without even bothering to resolve PATH. This results in the message:
+        // error: unable to execute command: Executable "" doesn't exist!
+        // So we use the expandArg0 variant of ChildProcess to give them a helping hand.
+        .expand_arg0 = .expand,
+    }) catch |err| switch (err) {
         error.OutOfMemory => return error.OutOfMemory,
         else => return error.UnableToSpawnCCompiler,
     };