Jonathan Marler před 1 měsícem
rodič
revize
a1226e6352
4 změnil soubory, kde provedl 665 přidání a 483 odebrání
  1. 498 33
      build.zig
  2. 1 1
      build.zig.zon
  3. 166 0
      runtest.zig
  4. 0 449
      test.zig

+ 498 - 33
build.zig

@@ -19,18 +19,10 @@ pub fn build(b: *std.Build) !void {
     };
 
     const test_step = b.step("test", "test the executable");
-    {
-        const exe = b.addExecutable(.{
-            .name = "test",
-            .root_source_file = b.path("test.zig"),
-            .target = target,
-            .optimize = optimize,
-        });
-        const run_cmd = b.addRunArtifact(exe);
-        run_cmd.addArtifactArg(zigup_exe_native);
-        run_cmd.addDirectoryArg(b.path("scratch/native"));
-        test_step.dependOn(&run_cmd.step);
-    }
+
+    addTests(b, target, zigup_exe_native, test_step, .{
+        .make_build_steps = true,
+    });
 
     const unzip_step = b.step(
         "unzip",
@@ -74,7 +66,7 @@ pub fn build(b: *std.Build) !void {
     ci_step.dependOn(test_step);
     ci_step.dependOn(unzip_step);
     ci_step.dependOn(zip_step);
-    try ci(b, ci_step, test_step, host_zip_exe);
+    try ci(b, ci_step, host_zip_exe);
 }
 
 fn addZigupExe(
@@ -113,7 +105,6 @@ fn addZigupExe(
 fn ci(
     b: *std.Build,
     ci_step: *std.Build.Step,
-    test_step: *std.Build.Step,
     host_zip_exe: *std.Build.Step.Compile,
 ) !void {
     const ci_targets = [_][]const u8{
@@ -132,8 +123,6 @@ fn ci(
     const make_archive_step = b.step("archive", "Create CI archives");
     ci_step.dependOn(make_archive_step);
 
-    var previous_test_step = test_step;
-
     for (ci_targets) |ci_target_str| {
         const target = b.resolveTargetQuery(try std.Target.Query.parse(
             .{ .arch_os_abi = ci_target_str },
@@ -147,27 +136,17 @@ fn ci(
         });
         ci_step.dependOn(&zigup_exe_install.step);
 
-        const test_exe = b.addExecutable(.{
-            .name = b.fmt("test-{s}", .{ci_target_str}),
-            .root_source_file = b.path("test.zig"),
-            .target = target,
-            .optimize = optimize,
+        const target_test_step = b.step(b.fmt("test-{s}", .{ci_target_str}), "");
+        addTests(b, target, zigup_exe, target_test_step, .{
+            .make_build_steps = false,
+            // This doesn't seem to be working, so we're only adding these tests
+            // as a dependency if we see the arch is compatible beforehand
+            .failing_to_execute_foreign_is_an_error = false,
         });
-        const run_cmd = b.addRunArtifact(test_exe);
-        run_cmd.addArtifactArg(zigup_exe);
-        run_cmd.addDirectoryArg(b.path(b.fmt("scratch/{s}", .{ci_target_str})));
-
-        // This doesn't seem to be working, so I've added a pre-check below
-        run_cmd.failing_to_execute_foreign_is_an_error = false;
         const os_compatible = (builtin.os.tag == target.result.os.tag);
         const arch_compatible = (builtin.cpu.arch == target.result.cpu.arch);
         if (os_compatible and arch_compatible) {
-            ci_step.dependOn(&run_cmd.step);
-
-            // prevent tests from running at the same time so their output
-            // doesn't mangle each other.
-            run_cmd.step.dependOn(previous_test_step);
-            previous_test_step = &run_cmd.step;
+            ci_step.dependOn(target_test_step);
         }
 
         if (builtin.os.tag == .linux) {
@@ -219,3 +198,489 @@ fn makeCiArchiveStep(
     tar.step.dependOn(&exe_install.step);
     return &tar.step;
 }
+
+const SharedTestOptions = struct {
+    make_build_steps: bool,
+    failing_to_execute_foreign_is_an_error: bool = true,
+};
+fn addTests(
+    b: *std.Build,
+    target: std.Build.ResolvedTarget,
+    zigup_exe: *std.Build.Step.Compile,
+    test_step: *std.Build.Step,
+    shared_options: SharedTestOptions,
+) void {
+    const runtest_exe = b.addExecutable(.{
+        .name = "runtest",
+        .root_source_file = b.path("runtest.zig"),
+        .target = target,
+    });
+    const tests: Tests = .{
+        .b = b,
+        .test_step = test_step,
+        .zigup_exe = zigup_exe,
+        .runtest_exe = runtest_exe,
+        .shared_options = shared_options,
+    };
+
+    tests.addWithClean(.{
+        .name = "test-usage-h",
+        .argv = &.{"-h"},
+        .check = .{ .expect_stderr_match = "Usage" },
+    });
+    tests.addWithClean(.{
+        .name = "test-usage-help",
+        .argv = &.{"--help"},
+        .check = .{ .expect_stderr_match = "Usage" },
+    });
+
+    tests.addWithClean(.{
+        .name = "test-fetch-index",
+        .argv = &.{"fetch-index"},
+        .checks = &.{
+            .{ .expect_stdout_match = "master" },
+            .{ .expect_stdout_match = "version" },
+            .{ .expect_stdout_match = "0.13.0" },
+        },
+    });
+
+    tests.addWithClean(.{
+        .name = "test-no-default",
+        .argv = &.{"default"},
+        .check = .{ .expect_stdout_exact = "<no-default>\n" },
+    });
+    tests.addWithClean(.{
+        .name = "test-default-master-not-fetched",
+        .argv = &.{ "default", "master" },
+        .check = .{ .expect_stderr_match = "master has not been fetched" },
+    });
+    tests.addWithClean(.{
+        .name = "test-default-0.7.0-not-fetched",
+        .argv = &.{ "default", "0.7.0" },
+        .check = .{ .expect_stderr_match = "error: compiler '0.7.0' is not installed\n" },
+    });
+
+    tests.addWithClean(.{
+        .name = "test-bad-version",
+        .argv = &.{ "THIS_ZIG_VERSION_DOES_NOT_EXIT" },
+        .checks = &.{
+            .{ .expect_stderr_match = "error: download '" },
+            .{ .expect_stderr_match = "' failed: " },
+        },
+    });
+
+    // NOTE: this test will eventually break when these builds are cleaned up,
+    //       we should support downloading from bazel and use that instead since
+    //       it should be more permanent
+    tests.addWithClean(.{
+        .name = "test-dev-version",
+        .argv = &.{ "0.14.0-dev.2465+70de2f3a7" },
+        .check = .{ .expect_stdout_exact = "" },
+    });
+
+    const _7 = tests.add(.{
+        .name = "test-7",
+        .argv = &.{"0.7.0"},
+        .check = .{ .expect_stdout_match = "" },
+    });
+    tests.addWithClean(.{
+        .name = "test-already-fetched-7",
+        .env = _7,
+        .argv = &.{ "fetch", "0.7.0" },
+        .check = .{ .expect_stderr_match = "already installed" },
+    });
+    tests.addWithClean(.{
+        .name = "test-get-default-7",
+        .env = _7,
+        .argv = &.{"default"},
+        .check = .{ .expect_stdout_exact = "0.7.0\n" },
+    });
+    tests.addWithClean(.{
+        .name = "test-get-default-7-no-path",
+        .env = _7,
+        .add_path = false,
+        .argv = &.{ "default", "0.7.0" },
+        .check = .{ .expect_stderr_match = " is not in PATH" },
+    });
+
+    // verify we print a nice error message if we can't update the symlink
+    // because it's a directory
+    tests.addWithClean(.{
+        .name = "test-get-default-7-path-link-is-directory",
+        .env = _7,
+        .setup_option = "path-link-is-directory",
+        .argv = &.{ "default", "0.7.0" },
+        .checks = switch (builtin.os.tag) {
+            .windows => &.{
+                .{ .expect_stderr_match = "unable to create the exe link, the path '" },
+                .{ .expect_stderr_match = "' is a directory" },
+            },
+            else => &.{
+                .{ .expect_stderr_match = "unable to update/overwrite the 'zig' PATH symlink, the file '" },
+                .{ .expect_stderr_match = "' already exists and is not a symlink" },
+            },
+        },
+    });
+
+    const _7_and_8 = tests.add(.{
+        .name = "test-fetch-8",
+        .env = _7,
+        .argv = &.{ "fetch", "0.8.0" },
+    });
+    tests.addWithClean(.{
+        .name = "test-get-default-7-after-fetch-8",
+        .env = _7_and_8,
+        .argv = &.{"default"},
+        .check = .{ .expect_stdout_exact = "0.7.0\n" },
+    });
+    tests.addWithClean(.{
+        .name = "test-already-fetched-8",
+        .env = _7_and_8,
+        .argv = &.{ "fetch", "0.8.0" },
+        .check = .{ .expect_stderr_match = "already installed" },
+    });
+    const _7_and_default_8 = tests.add(.{
+        .name = "test-set-default-8",
+        .env = _7_and_8,
+        .argv = &.{ "default", "0.8.0" },
+        .check = .{ .expect_stdout_exact = "" },
+    });
+    tests.addWithClean(.{
+        .name = "test-7-after-default-8",
+        .env = _7_and_default_8,
+        .argv = &.{"0.7.0"},
+        .check = .{ .expect_stdout_exact = "" },
+    });
+
+    const master_7_and_8 = tests.add(.{
+        .name = "test-master",
+        .env = _7_and_8,
+        .argv = &.{"master"},
+        .check = .{ .expect_stdout_exact = "" },
+    });
+    tests.addWithClean(.{
+        .name = "test-already-fetched-master",
+        .env = master_7_and_8,
+        .argv = &.{ "fetch", "master" },
+        .check = .{ .expect_stderr_match = "already installed" },
+    });
+
+    tests.addWithClean(.{
+        .name = "test-default-after-master",
+        .env = master_7_and_8,
+        .argv = &.{"default"},
+        // master version could be anything so we won't check
+    });
+    tests.addWithClean(.{
+        .name = "test-default-master",
+        .env = master_7_and_8,
+        .argv = &.{ "default", "master" },
+    });
+    tests.addWithClean(.{
+        .name = "test-default-not-in-path",
+        .add_path = false,
+        .env = master_7_and_8,
+        .argv = &.{ "default", "master" },
+        .check = .{ .expect_stderr_match = " is not in PATH" },
+    });
+
+    // verify that we get an error if there is another compiler in the path
+    tests.addWithClean(.{
+        .name = "test-default-master-with-another-zig",
+        .setup_option = "another-zig",
+        .env = master_7_and_8,
+        .argv = &.{ "default", "master" },
+        .checks = &.{
+            .{ .expect_stderr_match = "error: zig compiler '" },
+            .{ .expect_stderr_match = "' is higher priority in PATH than the path-link '" },
+        },
+    });
+
+    {
+        const default8 = tests.add(.{
+            .name = "test-default8-with-another-zig",
+            .setup_option = "another-zig",
+            .env = master_7_and_8,
+            .argv = &.{ "default", "0.8.0" },
+            .checks = &.{
+                .{ .expect_stderr_match = "error: zig compiler '" },
+                .{ .expect_stderr_match = "' is higher priority in PATH than the path-link '" },
+            },
+        });
+        // default compiler should still be set
+        tests.addWithClean(.{
+            .name = "test-default8-even-with-another-zig",
+            .env = default8,
+            .argv = &.{ "default" },
+            .check = .{ .expect_stdout_exact = "0.8.0\n" },
+        });
+    }
+
+    tests.addWithClean(.{
+        .name = "test-list",
+        .env = master_7_and_8,
+        .argv = &.{"list"},
+        .checks = &.{
+            .{ .expect_stdout_match = "0.7.0\n" },
+            .{ .expect_stdout_match = "0.8.0\n" },
+        },
+    });
+
+    {
+        const default_8 = tests.add(.{
+            .name = "test-8-with-master",
+            .env = master_7_and_8,
+            .argv = &.{"0.8.0"},
+            .check = .{ .expect_stdout_exact = "" },
+        });
+        tests.addWithClean(.{
+            .name = "test-default-8",
+            .env = default_8,
+            .argv = &.{"default"},
+            .check = .{ .expect_stdout_exact = "0.8.0\n" },
+        });
+    }
+
+    tests.addWithClean(.{
+        .name = "test-run-8",
+        .env = master_7_and_8,
+        .argv = &.{ "run", "0.8.0", "version" },
+        .check = .{ .expect_stdout_exact = "0.8.0\n" },
+    });
+    tests.addWithClean(.{
+        .name = "test-run-doesnotexist",
+        .env = master_7_and_8,
+        .argv = &.{ "run", "doesnotexist", "version" },
+        .check = .{ .expect_stderr_exact = "error: compiler 'doesnotexist' does not exist, fetch it first with: zigup fetch doesnotexist\n" },
+    });
+
+
+    tests.addWithClean(.{
+        .name = "test-clean-default-master",
+        .env = master_7_and_8,
+        .argv = &.{"clean"},
+        .checks = &.{
+            .{ .expect_stderr_match = "keeping '" },
+            .{ .expect_stderr_match = "' (is default compiler)\n" },
+            .{ .expect_stderr_match = "deleting '" },
+            .{ .expect_stderr_match = "0.7.0'\n" },
+            .{ .expect_stderr_match = "0.8.0'\n" },
+            .{ .expect_stdout_exact = "" },
+        },
+    });
+
+    {
+        const default7 = tests.add(.{
+            .name = "test-set-default-7",
+            .env = master_7_and_8,
+            .argv = &.{ "default", "0.7.0" },
+            .checks = &.{
+                .{ .expect_stdout_exact = "" },
+            },
+        });
+        tests.addWithClean(.{
+            .name = "test-clean-default-7",
+            .env = default7,
+            .argv = &.{"clean"},
+            .checks = &.{
+                .{ .expect_stderr_match = "keeping '" },
+                .{ .expect_stderr_match = "' (it is master)\n" },
+                .{ .expect_stderr_match = "keeping '0.7.0' (is default compiler)\n" },
+                .{ .expect_stderr_match = "deleting '" },
+                .{ .expect_stderr_match = "0.8.0'\n" },
+                .{ .expect_stdout_exact = "" },
+            },
+        });
+    }
+
+    {
+        const keep8 = tests.add(.{
+            .name = "test-keep8",
+            .env = master_7_and_8,
+            .argv = &.{ "keep", "0.8.0" },
+            .check = .{ .expect_stdout_exact = "" },
+        });
+
+        {
+            const keep8_default_7 = tests.add(.{
+                .name = "test-set-default-7-keep8",
+                .env = keep8,
+                .argv = &.{ "default", "0.7.0" },
+                .checks = &.{
+                    .{ .expect_stdout_exact = "" },
+                },
+            });
+            tests.addWithClean(.{
+                .name = "test-clean-default-7-keep8",
+                .env = keep8_default_7,
+                .argv = &.{"clean"},
+                .checks = &.{
+                    .{ .expect_stderr_match = "keeping '" },
+                    .{ .expect_stderr_match = "' (it is master)\n" },
+                    .{ .expect_stderr_match = "keeping '0.7.0' (is default compiler)\n" },
+                    .{ .expect_stderr_match = "keeping '0.8.0' (has keep file)\n" },
+                    .{ .expect_stdout_exact = "" },
+                },
+            });
+            tests.addWithClean(.{
+                .name = "test-clean-master",
+                .env = keep8_default_7,
+                .argv = &.{"clean", "master"},
+                .checks = &.{
+                    .{ .expect_stderr_match = "deleting '" },
+                    .{ .expect_stderr_match = "master'\n" },
+                    .{ .expect_stdout_exact = "" },
+                },
+            });
+        }
+
+        const after_clean = tests.add(.{
+            .name = "test-clean-keep8",
+            .env = keep8,
+            .argv = &.{"clean"},
+            .checks = &.{
+                .{ .expect_stderr_match = "keeping '" },
+                .{ .expect_stderr_match = "' (is default compiler)\n" },
+                .{ .expect_stderr_match = "keeping '0.8.0' (has keep file)\n" },
+                .{ .expect_stderr_match = "deleting '" },
+                .{ .expect_stderr_match = "0.7.0'\n" },
+            },
+        });
+
+        tests.addWithClean(.{
+            .name = "test-set-default-7-after-clean",
+            .env = after_clean,
+            .argv = &.{ "default", "0.7.0" },
+            .checks = &.{
+                .{ .expect_stderr_match = "error: compiler '0.7.0' is not installed\n" },
+            },
+        });
+
+        const default8 = tests.add(.{
+            .name = "test-set-default-8-after-clean",
+            .env = after_clean,
+            .argv = &.{ "default", "0.8.0" },
+            .checks = &.{
+                .{ .expect_stdout_exact = "" },
+            },
+        });
+
+
+        tests.addWithClean(.{
+            .name = "test-clean8-as-default",
+            .env = default8,
+            .argv = &.{ "clean", "0.8.0" },
+            .checks = &.{
+                .{ .expect_stderr_match = "error: cannot clean '0.8.0' (is default compiler)\n" },
+            },
+        });
+
+        const after_clean8 = tests.add(.{
+            .name = "test-clean8",
+            .env = after_clean,
+            .argv = &.{ "clean", "0.8.0" },
+            .checks = &.{
+                .{ .expect_stderr_match = "deleting '" },
+                .{ .expect_stderr_match = "0.8.0'\n" },
+                .{ .expect_stdout_exact = "" },
+            },
+        });
+        tests.addWithClean(.{
+            .name = "test-clean-after-clean8",
+            .env = after_clean8,
+            .argv = &.{"clean"},
+            .checks = &.{
+                .{ .expect_stderr_match = "keeping '" },
+                .{ .expect_stderr_match = "' (is default compiler)\n" },
+                .{ .expect_stdout_exact = "" },
+            },
+        });
+    }
+}
+
+const native_exe_ext = builtin.os.tag.exeFileExt(builtin.cpu.arch);
+
+const TestOptions = struct {
+    name: []const u8,
+    add_path: bool = true,
+    env: ?std.Build.LazyPath = null,
+    setup_option: []const u8 = "no-extra-setup",
+    argv: []const []const u8,
+    check: ?std.Build.Step.Run.StdIo.Check = null,
+    checks: []const std.Build.Step.Run.StdIo.Check = &.{},
+};
+
+const Tests = struct {
+    b: *std.Build,
+    test_step: *std.Build.Step,
+    zigup_exe: *std.Build.Step.Compile,
+    runtest_exe: *std.Build.Step.Compile,
+    shared_options: SharedTestOptions,
+
+    fn addWithClean(tests: Tests, opt: TestOptions) void {
+        _  = tests.addCommon(opt, .yes_clean);
+    }
+    fn add(tests: Tests, opt: TestOptions) std.Build.LazyPath {
+        return tests.addCommon(opt, .no_clean);
+    }
+    fn addCommon(tests: Tests, opt: TestOptions, clean_opt: enum { no_clean, yes_clean }) std.Build.LazyPath {
+        const b = tests.b;
+        const run = std.Build.Step.Run.create(b, b.fmt("run {s}", .{opt.name}));
+        run.failing_to_execute_foreign_is_an_error = tests.shared_options.failing_to_execute_foreign_is_an_error;
+        run.addArtifactArg(tests.runtest_exe);
+        run.addArg(opt.name);
+        run.addArg(if (opt.add_path) "--with-path" else "--no-path");
+        if (opt.env) |env| {
+            run.addDirectoryArg(env);
+        } else {
+            run.addArg("--no-input-environment");
+        }
+        const out_env = run.addOutputDirectoryArg(opt.name);
+        run.addArg(opt.setup_option);
+        run.addFileArg(tests.zigup_exe.getEmittedBin());
+        run.addArgs(opt.argv);
+        if (opt.check) |check| {
+            run.addCheck(check);
+        }
+        for (opt.checks) |check| {
+            run.addCheck(check);
+        }
+
+        const test_step: *std.Build.Step = switch (clean_opt) {
+            .no_clean => &run.step,
+            .yes_clean => &CleanDir.create(tests.b, out_env).step,
+        };
+
+        if (tests.shared_options.make_build_steps) {
+            b.step(opt.name, "").dependOn(test_step);
+        }
+        tests.test_step.dependOn(test_step);
+
+        return out_env;
+    }
+};
+
+const CleanDir = struct {
+    step: std.Build.Step,
+    dir_path: std.Build.LazyPath,
+    pub fn create(owner: *std.Build, path: std.Build.LazyPath) *CleanDir {
+        const clean_dir = owner.allocator.create(CleanDir) catch @panic("OOM");
+        clean_dir.* = .{
+            .step = std.Build.Step.init(.{
+                .id = .custom,
+                .name = owner.fmt("CleanDir {s}", .{path.getDisplayName()}),
+                .owner = owner,
+                .makeFn = make,
+            }),
+            .dir_path = path.dupe(owner),
+        };
+        path.addStepDependencies(&clean_dir.step);
+        return clean_dir;
+    }
+    fn make(step: *std.Build.Step, prog_node: std.Progress.Node) !void {
+        _ = prog_node;
+        const b = step.owner;
+        const clean_dir: *CleanDir = @fieldParentPtr("step", step);
+        try b.build_root.handle.deleteTree(clean_dir.dir_path.getPath(b));
+    }
+};

+ 1 - 1
build.zig.zon

@@ -8,7 +8,7 @@
         "build.zig",
         "build.zig.zon",
         "fixdeletetree.zig",
-        "test.zig",
+        "runtest.zig",
         "win32exelink.zig",
         "zigup.zig",
     },

+ 166 - 0
runtest.zig

@@ -0,0 +1,166 @@
+const builtin = @import("builtin");
+const std = @import("std");
+
+const fixdeletetree = @import("fixdeletetree.zig");
+
+const exe_ext = builtin.os.tag.exeFileExt(builtin.cpu.arch);
+
+pub fn main() !void {
+    var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+    const arena = arena_instance.allocator();
+    const all_args = try std.process.argsAlloc(arena);
+    if (all_args.len < 7) @panic("not enough cmdline args");
+
+    const test_name = all_args[1];
+    const add_path_option = all_args[2];
+    const in_env_dir = all_args[3];
+    const out_env_dir = all_args[4];
+    const setup_option = all_args[5];
+    const zigup_exe = all_args[6];
+    const zigup_args = all_args[7..];
+
+    const add_path = blk: {
+        if (std.mem.eql(u8, add_path_option, "--with-path")) break :blk true;
+        if (std.mem.eql(u8, add_path_option, "--no-path")) break :blk false;
+        std.log.err("expected '--with-path' or '--no-path' but got '{s}'", .{add_path_option});
+        std.process.exit(0xff);
+    };
+
+    try fixdeletetree.deleteTree(std.fs.cwd(), out_env_dir);
+    try std.fs.cwd().makeDir(out_env_dir);
+
+    // make a file named after the test so we can find this directory in the cache
+    _ = test_name;
+    // {
+    //     const test_marker_file = try std.fs.path.join(arena, &.{ out_env_dir, test_name});
+    //     defer arena.free(test_marker_file);
+    //     var file = try std.fs.cwd().createFile(test_marker_file, .{});
+    //     defer file.close();
+    //     try file.writer().print("this file marks this directory as the output for test: {s}\n", .{test_name});
+    // }
+
+    const path_link = try std.fs.path.join(arena, &.{ out_env_dir, "zig" ++ exe_ext });
+    const install_dir = try std.fs.path.join(arena, &.{ out_env_dir, "install" });
+
+    if (std.mem.eql(u8, in_env_dir, "--no-input-environment")) {
+        try std.fs.cwd().makeDir(install_dir);
+    } else {
+        try copyEnvDir(arena, in_env_dir, out_env_dir, in_env_dir, out_env_dir);
+    }
+
+    var maybe_second_bin_dir: ?[]const u8 = null;
+
+    if (std.mem.eql(u8, setup_option, "no-extra-setup")) {
+        // nothing extra to setup
+    } else if (std.mem.eql(u8, setup_option, "path-link-is-directory")) {
+        if (std.fs.cwd().access(path_link, .{})) {
+            try std.fs.cwd().deleteFile(path_link);
+        } else |err| switch (err) {
+            error.FileNotFound => {},
+            else => |e| return e,
+        }
+        try std.fs.cwd().makeDir(path_link);
+    } else if (std.mem.eql(u8, setup_option, "another-zig")) {
+        maybe_second_bin_dir = try std.fs.path.join(arena, &.{ out_env_dir, "bin2" });
+        try std.fs.cwd().makeDir(maybe_second_bin_dir.?);
+
+        const fake_zig = try std.fs.path.join(arena, &.{
+            maybe_second_bin_dir.?,
+            "zig" ++ comptime builtin.target.exeFileExt(),
+        });
+        defer arena.free(fake_zig);
+        var file = try std.fs.cwd().createFile(fake_zig, .{});
+        defer file.close();
+        try file.writer().writeAll("a fake executable");
+    } else {
+        std.log.err("unknown setup option '{s}'", .{setup_option});
+        std.process.exit(0xff);
+    }
+
+    var argv = std.ArrayList([]const u8).init(arena);
+    try argv.append(zigup_exe);
+    try argv.append("--path-link");
+    try argv.append(path_link);
+    try argv.append("--install-dir");
+    try argv.append(install_dir);
+    try argv.appendSlice(zigup_args);
+
+    var child = std.process.Child.init(argv.items, arena);
+
+    if (add_path) {
+        var env_map = try std.process.getEnvMap(arena);
+        // make sure the directory with our path-link comes first in PATH
+        var new_path = std.ArrayList(u8).init(arena);
+        if (maybe_second_bin_dir) |second_bin_dir| {
+            try new_path.appendSlice(second_bin_dir);
+            try new_path.append(std.fs.path.delimiter);
+        }
+        try new_path.appendSlice(out_env_dir);
+        try new_path.append(std.fs.path.delimiter);
+        if (env_map.get("PATH")) |path| {
+            try new_path.appendSlice(path);
+        }
+        try env_map.put("PATH", new_path.items);
+        child.env_map = &env_map;
+    } else if (maybe_second_bin_dir) |_| @panic("invalid config");
+
+    try child.spawn();
+    const result = try child.wait();
+    switch (result) {
+        .Exited => |c| std.process.exit(c),
+        else => |sig| {
+            std.log.err("zigup terminated from '{s}' with {}", .{ @tagName(result), sig });
+            std.process.exit(0xff);
+        },
+    }
+}
+
+fn copyEnvDir(
+    allocator: std.mem.Allocator,
+    in_root: []const u8,
+    out_root: []const u8,
+    in_path: []const u8,
+    out_path: []const u8,
+) !void {
+    var in_dir = try std.fs.cwd().openDir(in_path, .{ .iterate = true });
+    defer in_dir.close();
+
+    var it = in_dir.iterate();
+    while (try it.next()) |entry| {
+        const in_sub_path = try std.fs.path.join(allocator, &.{ in_path, entry.name });
+        defer allocator.free(in_sub_path);
+        const out_sub_path = try std.fs.path.join(allocator, &.{ out_path, entry.name });
+        defer allocator.free(out_sub_path);
+        switch (entry.kind) {
+            .directory => {
+                try std.fs.cwd().makeDir(out_sub_path);
+                try copyEnvDir(allocator, in_root, out_root, in_sub_path, out_sub_path);
+            },
+            .file => try std.fs.cwd().copyFile(in_sub_path, std.fs.cwd(), out_sub_path, .{}),
+            .sym_link => {
+                var target_buf: [std.fs.max_path_bytes]u8 = undefined;
+                const in_target = try std.fs.cwd().readLink(in_sub_path, &target_buf);
+                var out_target_buf: [std.fs.max_path_bytes]u8 = undefined;
+                const out_target = blk: {
+                    if (std.fs.path.isAbsolute(in_target)) {
+                        if (!std.mem.startsWith(u8, in_target, in_root)) std.debug.panic(
+                            "expected symlink target to start with '{s}' but got '{s}'",
+                            .{ in_root, in_target },
+                        );
+                        break :blk try std.fmt.bufPrint(
+                            &out_target_buf,
+                            "{s}{s}",
+                            .{ out_root, in_target[in_root.len..] },
+                        );
+                    }
+                    break :blk in_target;
+                };
+
+                if (builtin.os.tag == .windows) @panic(
+                    "we got a symlink on windows?",
+                ) else try std.posix.symlink(out_target, out_sub_path);
+            },
+            else => std.debug.panic("copy {}", .{entry}),
+        }
+    }
+}

+ 0 - 449
test.zig

@@ -1,449 +0,0 @@
-const std = @import("std");
-const builtin = @import("builtin");
-const testing = std.testing;
-
-const sep = std.fs.path.sep_str;
-const path_env_sep = if (builtin.os.tag == .windows) ";" else ":";
-
-const fixdeletetree = @import("fixdeletetree.zig");
-
-var child_env_map: std.process.EnvMap = undefined;
-var path_env_ptr: *[]const u8 = undefined;
-fn setPathEnv(new_path: []const u8) void {
-    path_env_ptr.* = new_path;
-    std.log.info("PATH={s}", .{new_path});
-}
-
-// For some odd reason, the "zig version" output is different on macos
-const expected_zig_version_0_7_0 = if (builtin.os.tag == .macos) "0.7.0+9af53f8e0" else "0.7.0";
-
-pub fn main() !u8 {
-    var allocator_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
-    //defer allocator_instance.deinit();
-    const allocator = allocator_instance.allocator();
-
-    const all_cmdline_args = try std.process.argsAlloc(allocator);
-    if (all_cmdline_args.len <= 1) {
-        try std.io.getStdErr().writer().print("Usage: test ZIGUP_EXE TEST_DIR\n", .{});
-        return 0xff;
-    }
-    const cmdline_args = all_cmdline_args[1..];
-    if (cmdline_args.len != 2) {
-        std.log.err("expected 1 cmdline arg but got {}", .{cmdline_args.len});
-        return 0xff;
-    }
-
-    const zigup_src_exe = cmdline_args[0];
-    const test_dir = cmdline_args[1];
-    std.log.info("run zigup tests", .{});
-    std.log.info("zigup exe '{s}'", .{zigup_src_exe});
-    std.log.info("test directory '{s}'", .{test_dir});
-
-    if (!std.fs.path.isAbsolute(test_dir)) {
-        std.log.err("currently the test requires an absolute test directory path", .{});
-        return 0xff;
-    }
-
-    try fixdeletetree.deleteTree(std.fs.cwd(), test_dir);
-    try std.fs.cwd().makePath(test_dir);
-    const bin_dir = try std.fs.path.join(allocator, &.{ test_dir, "bin" });
-    try std.fs.cwd().makeDir(bin_dir);
-    const install_sub_path = if (builtin.os.tag == .windows) "bin\\zig" else "install";
-    const install_dir = try std.fs.path.join(allocator, &.{ test_dir, install_sub_path });
-    try std.fs.cwd().makeDir(install_dir);
-
-    const zigup = try std.fs.path.join(allocator, &.{
-        test_dir,
-        "bin",
-        "zigup" ++ comptime builtin.target.exeFileExt(),
-    });
-    try std.fs.cwd().copyFile(
-        zigup_src_exe,
-        std.fs.cwd(),
-        zigup,
-        .{},
-    );
-    if (builtin.os.tag == .windows) {
-        const zigup_src_pdb = try std.mem.concat(allocator, u8, &.{
-            zigup_src_exe[0 .. zigup_src_exe.len - 4],
-            ".pdb",
-        });
-        defer allocator.free(zigup_src_pdb);
-        const zigup_pdb = try std.fs.path.join(allocator, &.{ test_dir, "bin\\zigup.pdb" });
-        defer allocator.free(zigup_pdb);
-        try std.fs.cwd().copyFile(zigup_src_pdb, std.fs.cwd(), zigup_pdb, .{});
-    }
-
-    const install_args = if (builtin.os.tag == .windows) [_][]const u8{} else [_][]const u8{
-        "--install-dir", install_dir,
-    };
-    const zigup_args = &[_][]const u8{zigup} ++ install_args;
-
-    const path_link = try std.fs.path.join(allocator, &.{ bin_dir, comptime "zig" ++ builtin.target.exeFileExt() });
-    defer allocator.free(path_link);
-
-    // add our scratch/bin directory to PATH
-    child_env_map = try std.process.getEnvMap(allocator);
-    path_env_ptr = child_env_map.getPtr("PATH") orelse {
-        std.log.err("the PATH environment variable does not exist?", .{});
-        return 1;
-    };
-
-    const original_path_env = path_env_ptr.*;
-    setPathEnv(try std.mem.concat(allocator, u8, &.{ bin_dir, path_env_sep, original_path_env }));
-
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{ "default", "master" });
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "master has not been fetched"));
-    }
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{"-h"});
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "Usage"));
-    }
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{"--help"});
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "Usage"));
-    }
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{"default"});
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try passOrDumpAndThrow(result);
-        try testing.expect(std.mem.eql(u8, result.stdout, "<no-default>\n"));
-    }
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{"fetch-index"});
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try passOrDumpAndThrow(result);
-        try testing.expect(std.mem.containsAtLeast(u8, result.stdout, 1, "master"));
-    }
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{ "default", "0.7.0" });
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        dumpExecResult(result);
-        switch (result.term) {
-            .Exited => |code| try testing.expectEqual(@as(u8, 1), code),
-            else => |term| std.debug.panic("unexpected exit {}", .{term}),
-        }
-        try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "error: compiler '0.7.0' is not installed\n"));
-    }
-    try runNoCapture(zigup_args ++ &[_][]const u8{"0.7.0"});
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{"default"});
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try passOrDumpAndThrow(result);
-        dumpExecResult(result);
-        try testing.expect(std.mem.eql(u8, result.stdout, "0.7.0\n"));
-    }
-
-    // verify we print a nice error message if we can't update the symlink
-    // because it's a directory
-    {
-        const zig_exe_link = try std.fs.path.join(allocator, &.{ bin_dir, "zig" ++ comptime builtin.target.exeFileExt() });
-        defer allocator.free(zig_exe_link);
-
-        if (std.fs.cwd().access(zig_exe_link, .{})) {
-            try std.fs.cwd().deleteFile(zig_exe_link);
-        } else |err| switch (err) {
-            error.FileNotFound => {},
-            else => |e| return e,
-        }
-        try std.fs.cwd().makeDir(zig_exe_link);
-
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{ "default", "0.7.0" });
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        dumpExecResult(result);
-        switch (result.term) {
-            .Exited => |code| try testing.expectEqual(@as(u8, 1), code),
-            else => |term| std.debug.panic("unexpected exit {}", .{term}),
-        }
-        if (builtin.os.tag == .windows) {
-            try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "unable to create the exe link, the path '"));
-            try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "' is a directory"));
-        } else {
-            try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "unable to update/overwrite the 'zig' PATH symlink, the file '"));
-            try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "' already exists and is not a symlink"));
-        }
-
-        try std.fs.cwd().deleteDir(zig_exe_link);
-    }
-
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{ "fetch", "0.7.0" });
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try passOrDumpAndThrow(result);
-        try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "already installed"));
-    }
-    try runNoCapture(zigup_args ++ &[_][]const u8{"master"});
-    try runNoCapture(zigup_args ++ &[_][]const u8{"0.8.0"});
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{"default"});
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try passOrDumpAndThrow(result);
-        dumpExecResult(result);
-        try testing.expect(std.mem.eql(u8, result.stdout, "0.8.0\n"));
-    }
-    {
-        const save_path_env = path_env_ptr.*;
-        defer setPathEnv(save_path_env);
-        setPathEnv("");
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{ "default", "master" });
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, " is not in PATH"));
-    }
-    try runNoCapture(zigup_args ++ &[_][]const u8{ "default", "master" });
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{"list"});
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try passOrDumpAndThrow(result);
-        try testing.expect(std.mem.containsAtLeast(u8, result.stdout, 1, "0.7.0"));
-        try testing.expect(std.mem.containsAtLeast(u8, result.stdout, 1, "0.8.0"));
-    }
-    try runNoCapture(zigup_args ++ &[_][]const u8{ "default", "0.7.0" });
-    try testing.expectEqual(@as(u32, 3), try getCompilerCount(install_dir));
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{ "run", "0.8.0", "version" });
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try testing.expectEqualSlices(u8, "0.8.0\n", result.stdout);
-    }
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{ "run", "doesnotexist", "version" });
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try testing.expectEqualSlices(u8, "error: compiler 'doesnotexist' does not exist, fetch it first with: zigup fetch doesnotexist\n", result.stderr);
-    }
-    try runNoCapture(zigup_args ++ &[_][]const u8{ "keep", "0.8.0" });
-    // doesn't delete anything because we have keepfile and master doens't get deleted
-    try runNoCapture(zigup_args ++ &[_][]const u8{"clean"});
-    try testing.expectEqual(@as(u32, 3), try getCompilerCount(install_dir));
-
-    // Just make a directory to trick zigup into thinking there is another compiler so we don't have to wait for it to download/install
-    try makeDir(test_dir, install_sub_path ++ sep ++ "0.9.0");
-    try testing.expectEqual(@as(u32, 4), try getCompilerCount(install_dir));
-    try runNoCapture(zigup_args ++ &[_][]const u8{"clean"});
-    try testing.expectEqual(@as(u32, 3), try getCompilerCount(install_dir));
-
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{ "clean", "0.8.0" });
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try passOrDumpAndThrow(result);
-        try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "deleting "));
-        try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "0.8.0"));
-    }
-    try testing.expectEqual(@as(u32, 2), try getCompilerCount(install_dir));
-
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{"clean"});
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try passOrDumpAndThrow(result);
-        try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "it is master"));
-    }
-    try testing.expectEqual(@as(u32, 2), try getCompilerCount(install_dir));
-
-    try runNoCapture(zigup_args ++ &[_][]const u8{"master"});
-    try testing.expectEqual(@as(u32, 2), try getCompilerCount(install_dir));
-
-    {
-        const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{"DOESNOTEXST"});
-        defer {
-            allocator.free(result.stdout);
-            allocator.free(result.stderr);
-        }
-        try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "download"));
-        try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "failed"));
-    }
-    try testing.expectEqual(@as(u32, 2), try getCompilerCount(install_dir));
-
-    // verify that we get an error if there is another compiler in the path
-    {
-        const bin2_dir = try std.fs.path.join(allocator, &.{ test_dir, "bin2" });
-        defer allocator.free(bin2_dir);
-        try std.fs.cwd().makeDir(bin2_dir);
-
-        const previous_path = path_env_ptr.*;
-
-        {
-            const fake_zig = try std.fs.path.join(allocator, &.{
-                bin2_dir,
-                "zig" ++ comptime builtin.target.exeFileExt(),
-            });
-            defer allocator.free(fake_zig);
-            var file = try std.fs.cwd().createFile(fake_zig, .{});
-            defer file.close();
-            try file.writer().writeAll("a fake executable");
-        }
-
-        setPathEnv(try std.mem.concat(allocator, u8, &.{ bin2_dir, path_env_sep, previous_path }));
-        defer setPathEnv(previous_path);
-
-        // verify zig isn't currently on 0.7.0 before we set it as the default
-        try checkZigVersion(allocator, path_link, expected_zig_version_0_7_0, .not_equal);
-
-        {
-            const result = try runCaptureOuts(allocator, zigup_args ++ &[_][]const u8{ "default", "0.7.0" });
-            defer {
-                allocator.free(result.stdout);
-                allocator.free(result.stderr);
-            }
-            std.log.info("output: {s}", .{result.stderr});
-            try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "error: zig compiler '"));
-            try testing.expect(std.mem.containsAtLeast(u8, result.stderr, 1, "' is higher priority in PATH than the path-link '"));
-        }
-
-        // the path link should still be updated even though it's in a lower path priority.
-        // Verify zig points to the new defult version we just set.
-        try checkZigVersion(allocator, path_link, expected_zig_version_0_7_0, .equal);
-    }
-
-    // verify a dev build
-    // NOTE: this test will eventually break when these builds are cleaned up,
-    //       we should support downloading from bazel and use that instead since
-    //       it should be more permanent
-    try runNoCapture(zigup_args ++ &[_][]const u8{"0.14.0-dev.2465+70de2f3a7"});
-
-    std.log.info("Success", .{});
-    return 0;
-}
-
-fn makeDir(dir_path: []const u8, sub_path: []const u8) !void {
-    var dir = try std.fs.cwd().openDir(dir_path, .{});
-    defer dir.close();
-    try dir.makeDir(sub_path);
-}
-
-fn checkZigVersion(allocator: std.mem.Allocator, zig: []const u8, compare: []const u8, want_equal: enum { not_equal, equal }) !void {
-    const result = try runCaptureOuts(allocator, &[_][]const u8{ zig, "version" });
-    defer {
-        allocator.free(result.stdout);
-        allocator.free(result.stderr);
-    }
-    try passOrDumpAndThrow(result);
-
-    const actual_version = std.mem.trimRight(u8, result.stdout, "\r\n");
-    const actual_equal = std.mem.eql(u8, compare, actual_version);
-    const expected_equal = switch (want_equal) {
-        .not_equal => false,
-        .equal => true,
-    };
-    if (expected_equal != actual_equal) {
-        const prefix: []const u8 = if (expected_equal) "" else " NOT";
-        std.log.info("expected zig version to{s} be '{s}', but is '{s}'", .{ prefix, compare, actual_version });
-        return error.TestUnexpectedResult;
-    }
-}
-
-fn getCompilerCount(install_dir: []const u8) !u32 {
-    var dir = try std.fs.cwd().openDir(install_dir, .{ .iterate = true });
-    defer dir.close();
-    var it = dir.iterate();
-    var count: u32 = 0;
-    while (try it.next()) |entry| {
-        if (entry.kind == .directory) {
-            count += 1;
-        } else {
-            if (builtin.os.tag == .windows) {
-                try testing.expect(entry.kind == .file);
-            } else {
-                try testing.expect(entry.kind == .sym_link);
-            }
-        }
-    }
-    return count;
-}
-
-fn trailNl(s: []const u8) []const u8 {
-    return if (s.len == 0 or s[s.len - 1] != '\n') "\n" else "";
-}
-
-fn dumpExecResult(result: std.process.Child.RunResult) void {
-    if (result.stdout.len > 0) {
-        std.debug.print("--- STDOUT ---\n{s}{s}--------------\n", .{ result.stdout, trailNl(result.stdout) });
-    }
-    if (result.stderr.len > 0) {
-        std.debug.print("--- STDERR ---\n{s}{s}--------------\n", .{ result.stderr, trailNl(result.stderr) });
-    }
-}
-
-fn runNoCapture(argv: []const []const u8) !void {
-    var arena_store = std.heap.ArenaAllocator.init(std.heap.page_allocator);
-    defer arena_store.deinit();
-    const result = try runCaptureOuts(arena_store.allocator(), argv);
-    dumpExecResult(result);
-    try passOrThrow(result.term);
-}
-fn runCaptureOuts(allocator: std.mem.Allocator, argv: []const []const u8) !std.process.Child.RunResult {
-    {
-        const cmd = try std.mem.join(allocator, " ", argv);
-        defer allocator.free(cmd);
-        std.log.info("RUN: {s}", .{cmd});
-    }
-    return try std.process.Child.run(.{ .allocator = allocator, .argv = argv, .env_map = &child_env_map });
-}
-fn passOrThrow(term: std.process.Child.Term) error{ChildProcessFailed}!void {
-    if (!execResultPassed(term)) {
-        std.log.err("child process failed with {}", .{term});
-        return error.ChildProcessFailed;
-    }
-}
-fn passOrDumpAndThrow(result: std.process.Child.RunResult) error{ChildProcessFailed}!void {
-    if (!execResultPassed(result.term)) {
-        dumpExecResult(result);
-        std.log.err("child process failed with {}", .{result.term});
-        return error.ChildProcessFailed;
-    }
-}
-fn execResultPassed(term: std.process.Child.Term) bool {
-    switch (term) {
-        .Exited => |code| return code == 0,
-        else => return false,
-    }
-}