├── .gitattributes ├── .github └── workflows │ └── artifact.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── fixdeletetree.zig ├── runtest.zig ├── unzip.zig ├── win32exelink.zig ├── zigup.zig └── zip.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/artifact.yml: -------------------------------------------------------------------------------- 1 | name: Artifacts 2 | on: [pull_request, workflow_dispatch] 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | arch: [x86_64] 8 | os: [ubuntu-latest, macos-latest, windows-latest] 9 | fail-fast: false 10 | runs-on: ${{matrix.os}} 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: mlugg/setup-zig@v2 14 | - run: | 15 | zig build ci --summary all 16 | - if: ${{ matrix.os == 'ubuntu-latest' }} 17 | uses: actions/upload-artifact@v4 18 | with: 19 | name: zigup-archives 20 | path: | 21 | zig-out/zigup-aarch64-linux.tar.gz 22 | zig-out/zigup-aarch64-macos.tar.gz 23 | zig-out/zigup-aarch64-windows.zip 24 | zig-out/zigup-arm-linux.tar.gz 25 | zig-out/zigup-powerpc64le-linux.tar.gz 26 | zig-out/zigup-riscv64-linux.tar.gz 27 | zig-out/zigup-s390x-linux.tar.gz 28 | zig-out/zigup-x86-linux.tar.gz 29 | zig-out/zigup-x86-windows.tar.gz 30 | zig-out/zigup-x86_64-linux.tar.gz 31 | zig-out/zigup-x86_64-macos.tar.gz 32 | zig-out/zigup-x86_64-windows.zip 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .zig-cache/ 3 | zig-out/ 4 | /dep 5 | /scratch 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright 2022 Zigup Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zigup 2 | 3 | Download and manage zig compilers. 4 | 5 | > NOTE: I no longer use zigup. I've switched to using [anyzig](https://github.com/marler8997/anyzig) instead and recommend others do the same (here's [why](#why-anyzig)). Zigup will continue to be supported for those that just love it so much! 6 | 7 | # How to Install 8 | 9 | Go to https://marler8997.github.io/zigup and select your OS/Arch to get a download link and/or instructions to install via the command-line. 10 | 11 | Otherwise, you can manually find and download/extract the applicable archive from [Releases](https://github.com/marler8997/zigup/releases). It will contain a single static binary named `zigup`, unless you're on Windows in which case it's `zigup.exe`. 12 | 13 | # Usage 14 | 15 | ``` 16 | # fetch a compiler and set it as the default 17 | zigup 18 | zigup master 19 | zigup 0.6.0 20 | 21 | # fetch a compiler only (do not set it as default) 22 | zigup fetch 23 | zigup fetch master 24 | 25 | # print the default compiler version 26 | zigup default 27 | 28 | # set the default compiler 29 | zigup default 30 | 31 | # list the installed compiler versions 32 | zigup list 33 | 34 | # clean compilers that are not the default, not master, and not marked to keep. when a version is specified, it will clean that version 35 | zigup clean [] 36 | 37 | # mark a compiler to keep 38 | zigup keep 39 | 40 | # run a specific version of the compiler 41 | zigup run ... 42 | ``` 43 | 44 | # How the compilers are managed 45 | 46 | zigup stores each compiler in a global "install directory" in a versioned subdirectory. Run `zigup get-install-dir` to see what this PATH is on your system. You can change this default with `zigup set-install-dir PATH`. 47 | 48 | zigup makes the zig program available by creating an entry in a directory that occurs in the `PATH` environment variable. On posix systems this entry is a symlink to one of the `zig` executables in the install directory. On windows this is an executable that forwards invocations to one of the `zig` executables in the install directory. 49 | 50 | # Building 51 | 52 | Run `zig build` to build, `zig build test` to test and install with: 53 | ``` 54 | # install to a bin directory with 55 | cp zig-out/bin/zigup BIN_PATH 56 | ``` 57 | 58 | # TODO 59 | 60 | * set/remove compiler in current environment without overriding the system-wide version. 61 | 62 | # Dependencies 63 | 64 | On linux and macos, zigup depends on `tar` to extract the compiler archive files (this may change in the future). 65 | 66 | # Why Anyzig? 67 | 68 | Zigup helps you download/switch which version of zig is invoked when you run `zig`. In contrast, Anyzig is one universal `zig` executable that invokes the correct version of zig based on the current project. Anyzig came about from the realization that if you have `zig` installed system-wide, then it should work with any Zig project, not just those that happen to match the current version you've installed/enabled. Instead of manually switching versions yourself, it uses the `minimum_zig_version` field in `build.zig.zon`. An added benefit of anyzig is any project that uses it is guaranteed to have their zig version both documented and up-to-date. In practice, I've also found that anyzig frees some mental load because you no longer need to track which version of Zig each project is on, which version the system is on, and keeping the two in sync. 69 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | pub fn build(b: *std.Build) !void { 5 | const target = b.standardTargetOptions(.{}); 6 | const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseSafe }); 7 | 8 | const zigup_exe_native = blk: { 9 | const exe = addZigupExe(b, target, optimize); 10 | b.installArtifact(exe); 11 | const run_cmd = b.addRunArtifact(exe); 12 | run_cmd.step.dependOn(b.getInstallStep()); 13 | const run_step = b.step("run", "Run the app"); 14 | run_step.dependOn(&run_cmd.step); 15 | if (b.args) |args| { 16 | run_cmd.addArgs(args); 17 | } 18 | break :blk exe; 19 | }; 20 | 21 | const test_step = b.step("test", "test the executable"); 22 | 23 | addTests(b, target, zigup_exe_native, test_step, .{ 24 | .make_build_steps = true, 25 | }); 26 | 27 | const unzip_step = b.step( 28 | "unzip", 29 | "Build/install the unzip cmdline tool", 30 | ); 31 | 32 | { 33 | const unzip = b.addExecutable(.{ 34 | .name = "unzip", 35 | .root_source_file = b.path("unzip.zig"), 36 | .target = target, 37 | .optimize = optimize, 38 | }); 39 | const install = b.addInstallArtifact(unzip, .{}); 40 | unzip_step.dependOn(&install.step); 41 | } 42 | 43 | const zip_step = b.step( 44 | "zip", 45 | "Build/install the zip cmdline tool", 46 | ); 47 | { 48 | const zip = b.addExecutable(.{ 49 | .name = "zip", 50 | .root_source_file = b.path("zip.zig"), 51 | .target = target, 52 | .optimize = optimize, 53 | }); 54 | const install = b.addInstallArtifact(zip, .{}); 55 | zip_step.dependOn(&install.step); 56 | } 57 | 58 | const host_zip_exe = b.addExecutable(.{ 59 | .name = "zip", 60 | .root_source_file = b.path("zip.zig"), 61 | .target = b.graph.host, 62 | }); 63 | 64 | const ci_step = b.step("ci", "The build/test step to run on the CI"); 65 | ci_step.dependOn(b.getInstallStep()); 66 | ci_step.dependOn(test_step); 67 | ci_step.dependOn(unzip_step); 68 | ci_step.dependOn(zip_step); 69 | try ci(b, ci_step, host_zip_exe); 70 | } 71 | 72 | fn addZigupExe( 73 | b: *std.Build, 74 | target: std.Build.ResolvedTarget, 75 | optimize: std.builtin.Mode, 76 | ) *std.Build.Step.Compile { 77 | const win32exelink_mod: ?*std.Build.Module = blk: { 78 | if (target.result.os.tag == .windows) { 79 | const exe = b.addExecutable(.{ 80 | .name = "win32exelink", 81 | .root_source_file = b.path("win32exelink.zig"), 82 | .target = target, 83 | .optimize = optimize, 84 | }); 85 | break :blk b.createModule(.{ 86 | .root_source_file = exe.getEmittedBin(), 87 | }); 88 | } 89 | break :blk null; 90 | }; 91 | 92 | const exe = b.addExecutable(.{ 93 | .name = "zigup", 94 | .root_source_file = b.path("zigup.zig"), 95 | .target = target, 96 | .optimize = optimize, 97 | .strip = true, 98 | }); 99 | 100 | if (target.result.os.tag == .windows) { 101 | exe.root_module.addImport("win32exelink", win32exelink_mod.?); 102 | } 103 | return exe; 104 | } 105 | 106 | fn ci( 107 | b: *std.Build, 108 | ci_step: *std.Build.Step, 109 | host_zip_exe: *std.Build.Step.Compile, 110 | ) !void { 111 | const ci_targets = [_][]const u8{ 112 | "aarch64-linux", 113 | "aarch64-macos", 114 | "aarch64-windows", 115 | "arm-linux", 116 | "powerpc64le-linux", 117 | "riscv64-linux", 118 | "s390x-linux", 119 | "x86-linux", 120 | "x86-windows", 121 | "x86_64-linux", 122 | "x86_64-macos", 123 | "x86_64-windows", 124 | }; 125 | 126 | const make_archive_step = b.step("archive", "Create CI archives"); 127 | ci_step.dependOn(make_archive_step); 128 | 129 | for (ci_targets) |ci_target_str| { 130 | const target = b.resolveTargetQuery(try std.Target.Query.parse( 131 | .{ .arch_os_abi = ci_target_str }, 132 | )); 133 | const optimize: std.builtin.OptimizeMode = 134 | // Compile in ReleaseSafe on Windows for faster extraction 135 | if (target.result.os.tag == .windows) .ReleaseSafe else .Debug; 136 | const zigup_exe = addZigupExe(b, target, optimize); 137 | const zigup_exe_install = b.addInstallArtifact(zigup_exe, .{ 138 | .dest_dir = .{ .override = .{ .custom = ci_target_str } }, 139 | }); 140 | ci_step.dependOn(&zigup_exe_install.step); 141 | 142 | const target_test_step = b.step(b.fmt("test-{s}", .{ci_target_str}), ""); 143 | addTests(b, target, zigup_exe, target_test_step, .{ 144 | .make_build_steps = false, 145 | // This doesn't seem to be working, so we're only adding these tests 146 | // as a dependency if we see the arch is compatible beforehand 147 | .failing_to_execute_foreign_is_an_error = false, 148 | }); 149 | const os_compatible = (builtin.os.tag == target.result.os.tag); 150 | const arch_compatible = (builtin.cpu.arch == target.result.cpu.arch); 151 | if (os_compatible and arch_compatible) { 152 | ci_step.dependOn(target_test_step); 153 | } 154 | 155 | if (builtin.os.tag == .linux) { 156 | make_archive_step.dependOn(makeCiArchiveStep(b, ci_target_str, target.result, zigup_exe_install, host_zip_exe)); 157 | } 158 | } 159 | } 160 | 161 | fn makeCiArchiveStep( 162 | b: *std.Build, 163 | ci_target_str: []const u8, 164 | target: std.Target, 165 | exe_install: *std.Build.Step.InstallArtifact, 166 | host_zip_exe: *std.Build.Step.Compile, 167 | ) *std.Build.Step { 168 | const install_path = b.getInstallPath(.prefix, "."); 169 | 170 | if (target.os.tag == .windows) { 171 | const out_zip_file = b.pathJoin(&.{ 172 | install_path, 173 | b.fmt("zigup-{s}.zip", .{ci_target_str}), 174 | }); 175 | const zip = b.addRunArtifact(host_zip_exe); 176 | zip.addArg(out_zip_file); 177 | zip.addArg("zigup.exe"); 178 | zip.cwd = .{ .cwd_relative = b.getInstallPath( 179 | exe_install.dest_dir.?, 180 | ".", 181 | ) }; 182 | zip.step.dependOn(&exe_install.step); 183 | return &zip.step; 184 | } 185 | 186 | const targz = b.pathJoin(&.{ 187 | install_path, 188 | b.fmt("zigup-{s}.tar.gz", .{ci_target_str}), 189 | }); 190 | const tar = b.addSystemCommand(&.{ 191 | "tar", 192 | "-czf", 193 | targz, 194 | "zigup", 195 | }); 196 | tar.cwd = .{ .cwd_relative = b.getInstallPath( 197 | exe_install.dest_dir.?, 198 | ".", 199 | ) }; 200 | tar.step.dependOn(&exe_install.step); 201 | return &tar.step; 202 | } 203 | 204 | const SharedTestOptions = struct { 205 | make_build_steps: bool, 206 | failing_to_execute_foreign_is_an_error: bool = true, 207 | }; 208 | fn addTests( 209 | b: *std.Build, 210 | target: std.Build.ResolvedTarget, 211 | zigup_exe: *std.Build.Step.Compile, 212 | test_step: *std.Build.Step, 213 | shared_options: SharedTestOptions, 214 | ) void { 215 | const runtest_exe = b.addExecutable(.{ 216 | .name = "runtest", 217 | .root_source_file = b.path("runtest.zig"), 218 | .target = target, 219 | }); 220 | const tests: Tests = .{ 221 | .b = b, 222 | .test_step = test_step, 223 | .zigup_exe = zigup_exe, 224 | .runtest_exe = runtest_exe, 225 | .shared_options = shared_options, 226 | }; 227 | 228 | tests.addWithClean(.{ 229 | .name = "test-usage-h", 230 | .argv = &.{"-h"}, 231 | .check = .{ .expect_stderr_match = "Usage" }, 232 | }); 233 | tests.addWithClean(.{ 234 | .name = "test-usage-help", 235 | .argv = &.{"--help"}, 236 | .check = .{ .expect_stderr_match = "Usage" }, 237 | }); 238 | 239 | tests.addWithClean(.{ 240 | .name = "test-fetch-index", 241 | .argv = &.{"fetch-index"}, 242 | .checks = &.{ 243 | .{ .expect_stdout_match = "master" }, 244 | .{ .expect_stdout_match = "version" }, 245 | .{ .expect_stdout_match = "0.13.0" }, 246 | }, 247 | }); 248 | 249 | tests.addWithClean(.{ 250 | .name = "test-invalid-index-url", 251 | .argv = &.{ "fetch-index", "--index", "this-is-not-a-valid-url" }, 252 | .checks = &.{ 253 | .{ .expect_stderr_match = "error: could not download 'this-is-not-a-valid-url': the URL is invalid (InvalidFormat)" }, 254 | }, 255 | }); 256 | 257 | tests.addWithClean(.{ 258 | .name = "test-invalid-index-content", 259 | .argv = &.{ "fetch-index", "--index", "https://ziglang.org" }, 260 | .checks = &.{ 261 | .{ .expect_stderr_match = "failed to parse JSON content from index url 'https://ziglang.org' with " }, 262 | }, 263 | }); 264 | 265 | tests.addWithClean(.{ 266 | .name = "test-get-install-dir", 267 | .argv = &.{"get-install-dir"}, 268 | }); 269 | tests.addWithClean(.{ 270 | .name = "test-get-install-dir2", 271 | .argv = &.{ "--install-dir", "/a/fake/install/dir", "get-install-dir" }, 272 | .checks = &.{ 273 | .{ .expect_stdout_exact = "/a/fake/install/dir\n" }, 274 | }, 275 | }); 276 | tests.addWithClean(.{ 277 | .name = "test-set-install-dir-relative", 278 | .argv = &.{ "set-install-dir", "foo/bar" }, 279 | .checks = &.{ 280 | .{ .expect_stderr_match = "error: set-install-dir requires an absolute path" }, 281 | }, 282 | }); 283 | 284 | { 285 | // just has to be an absolute path that exists 286 | const install_dir = b.build_root.path.?; 287 | const with_install_dir = tests.add(.{ 288 | .name = "test-set-install-dir", 289 | .argv = &.{ "set-install-dir", install_dir }, 290 | }); 291 | tests.addWithClean(.{ 292 | .name = "test-get-install-dir3", 293 | .argv = &.{"get-install-dir"}, 294 | .env = .{ .dir = with_install_dir }, 295 | .checks = &.{ 296 | .{ .expect_stdout_exact = b.fmt("{s}\n", .{install_dir}) }, 297 | }, 298 | }); 299 | tests.addWithClean(.{ 300 | .name = "test-revert-install-dir", 301 | .argv = &.{"set-install-dir"}, 302 | .env = .{ .dir = with_install_dir }, 303 | }); 304 | } 305 | 306 | tests.addWithClean(.{ 307 | .name = "test-no-default", 308 | .argv = &.{"default"}, 309 | .check = .{ .expect_stdout_exact = "\n" }, 310 | }); 311 | tests.addWithClean(.{ 312 | .name = "test-default-master-not-fetched", 313 | .argv = &.{ "default", "master" }, 314 | .check = .{ .expect_stderr_match = "master has not been fetched" }, 315 | }); 316 | tests.addWithClean(.{ 317 | .name = "test-default-0.7.0-not-fetched", 318 | .argv = &.{ "default", "0.7.0" }, 319 | .check = .{ .expect_stderr_match = "error: compiler '0.7.0' is not installed\n" }, 320 | }); 321 | 322 | tests.addWithClean(.{ 323 | .name = "test-invalid-version", 324 | .argv = &.{"this-version-is-not-valid"}, 325 | .checks = &.{ 326 | .{ .expect_stderr_match = "error: invalid zig version 'this-version-is-not-valid', unable to create a download URL for it\n" }, 327 | }, 328 | }); 329 | 330 | const non_existent_version = "0.0.99"; 331 | tests.addWithClean(.{ 332 | .name = "test-valid-but-nonexistent-version", 333 | .argv = &.{non_existent_version}, 334 | .checks = &.{ 335 | .{ .expect_stderr_match = "error: could not download '" }, 336 | }, 337 | }); 338 | 339 | // NOTE: this test will eventually break when these builds are cleaned up, 340 | // we should support downloading from bazel and use that instead since 341 | // it should be more permanent 342 | if (false) tests.addWithClean(.{ 343 | .name = "test-dev-version", 344 | .argv = &.{"0.15.0-dev.621+a63f7875f"}, 345 | .check = .{ .expect_stdout_exact = "" }, 346 | }); 347 | 348 | const _7 = tests.add(.{ 349 | .name = "test-7", 350 | .argv = &.{"0.7.0"}, 351 | .check = .{ .expect_stdout_match = "" }, 352 | }); 353 | tests.addWithClean(.{ 354 | .name = "test-already-fetched-7", 355 | .env = .{ .dir = _7 }, 356 | .argv = &.{ "fetch", "0.7.0" }, 357 | .check = .{ .expect_stderr_match = "already installed" }, 358 | }); 359 | tests.addWithClean(.{ 360 | .name = "test-get-default-7", 361 | .env = .{ .dir = _7 }, 362 | .argv = &.{"default"}, 363 | .check = .{ .expect_stdout_exact = "0.7.0\n" }, 364 | }); 365 | tests.addWithClean(.{ 366 | .name = "test-get-default-7-no-path", 367 | .env = .{ .dir = _7 }, 368 | .add_path = false, 369 | .argv = &.{ "default", "0.7.0" }, 370 | .check = .{ .expect_stderr_match = " is not in PATH" }, 371 | }); 372 | 373 | // verify we print a nice error message if we can't update the symlink 374 | // because it's a directory 375 | tests.addWithClean(.{ 376 | .name = "test-get-default-7-path-link-is-directory", 377 | .env = .{ .dir = _7 }, 378 | .setup_option = "path-link-is-directory", 379 | .argv = &.{ "default", "0.7.0" }, 380 | .checks = switch (builtin.os.tag) { 381 | .windows => &.{ 382 | .{ .expect_stderr_match = "unable to create the exe link, the path '" }, 383 | .{ .expect_stderr_match = "' is a directory" }, 384 | }, 385 | else => &.{ 386 | .{ .expect_stderr_match = "unable to update/overwrite the 'zig' PATH symlink, the file '" }, 387 | .{ .expect_stderr_match = "' already exists and is not a symlink" }, 388 | }, 389 | }, 390 | }); 391 | 392 | const _7_and_8 = tests.add(.{ 393 | .name = "test-fetch-8", 394 | .env = .{ .dir = _7 }, 395 | .keep_compilers = "0.8.0", 396 | .argv = &.{ "fetch", "0.8.0" }, 397 | }); 398 | tests.addWithClean(.{ 399 | .name = "test-get-default-7-after-fetch-8", 400 | .env = .{ .dir = _7_and_8 }, 401 | .argv = &.{"default"}, 402 | .check = .{ .expect_stdout_exact = "0.7.0\n" }, 403 | }); 404 | tests.addWithClean(.{ 405 | .name = "test-already-fetched-8", 406 | .env = .{ .dir = _7_and_8 }, 407 | .argv = &.{ "fetch", "0.8.0" }, 408 | .check = .{ .expect_stderr_match = "already installed" }, 409 | }); 410 | const _7_and_default_8 = tests.add(.{ 411 | .name = "test-set-default-8", 412 | .env = .{ .dir = _7_and_8 }, 413 | .argv = &.{ "default", "0.8.0" }, 414 | .check = .{ .expect_stdout_exact = "" }, 415 | }); 416 | tests.addWithClean(.{ 417 | .name = "test-7-after-default-8", 418 | .env = .{ .dir = _7_and_default_8 }, 419 | .argv = &.{"0.7.0"}, 420 | .check = .{ .expect_stdout_exact = "" }, 421 | }); 422 | 423 | const master_7_and_8 = tests.add(.{ 424 | .name = "test-master", 425 | .env = .{ .dir = _7_and_8, .with_compilers = "0.8.0" }, 426 | .keep_compilers = "0.8.0", 427 | .argv = &.{"master"}, 428 | .check = .{ .expect_stdout_exact = "" }, 429 | }); 430 | tests.addWithClean(.{ 431 | .name = "test-already-fetched-master", 432 | .env = .{ .dir = master_7_and_8 }, 433 | .argv = &.{ "fetch", "master" }, 434 | .check = .{ .expect_stderr_match = "already installed" }, 435 | }); 436 | 437 | tests.addWithClean(.{ 438 | .name = "test-default-after-master", 439 | .env = .{ .dir = master_7_and_8 }, 440 | .argv = &.{"default"}, 441 | // master version could be anything so we won't check 442 | }); 443 | tests.addWithClean(.{ 444 | .name = "test-default-master", 445 | .env = .{ .dir = master_7_and_8 }, 446 | .argv = &.{ "default", "master" }, 447 | }); 448 | tests.addWithClean(.{ 449 | .name = "test-default-not-in-path", 450 | .add_path = false, 451 | .env = .{ .dir = master_7_and_8 }, 452 | .argv = &.{ "default", "master" }, 453 | .check = .{ .expect_stderr_match = " is not in PATH" }, 454 | }); 455 | 456 | // verify that we get an error if there is another compiler in the path 457 | tests.addWithClean(.{ 458 | .name = "test-default-master-with-another-zig", 459 | .setup_option = "another-zig", 460 | .env = .{ .dir = master_7_and_8 }, 461 | .argv = &.{ "default", "master" }, 462 | .checks = &.{ 463 | .{ .expect_stderr_match = "error: zig compiler '" }, 464 | .{ .expect_stderr_match = "' is higher priority in PATH than the path-link '" }, 465 | }, 466 | }); 467 | 468 | { 469 | const default8 = tests.add(.{ 470 | .name = "test-default8-with-another-zig", 471 | .setup_option = "another-zig", 472 | .env = .{ .dir = master_7_and_8 }, 473 | .argv = &.{ "default", "0.8.0" }, 474 | .checks = &.{ 475 | .{ .expect_stderr_match = "error: zig compiler '" }, 476 | .{ .expect_stderr_match = "' is higher priority in PATH than the path-link '" }, 477 | }, 478 | }); 479 | // default compiler should still be set 480 | tests.addWithClean(.{ 481 | .name = "test-default8-even-with-another-zig", 482 | .env = .{ .dir = default8 }, 483 | .argv = &.{"default"}, 484 | .check = .{ .expect_stdout_exact = "0.8.0\n" }, 485 | }); 486 | } 487 | 488 | tests.addWithClean(.{ 489 | .name = "test-list", 490 | .env = .{ .dir = master_7_and_8 }, 491 | .argv = &.{"list"}, 492 | .checks = &.{ 493 | .{ .expect_stdout_match = "0.7.0\n" }, 494 | .{ .expect_stdout_match = "0.8.0\n" }, 495 | }, 496 | }); 497 | 498 | { 499 | const default_8 = tests.add(.{ 500 | .name = "test-8-with-master", 501 | .env = .{ .dir = master_7_and_8 }, 502 | .argv = &.{"0.8.0"}, 503 | .check = .{ .expect_stdout_exact = "" }, 504 | }); 505 | tests.addWithClean(.{ 506 | .name = "test-default-8", 507 | .env = .{ .dir = default_8 }, 508 | .argv = &.{"default"}, 509 | .check = .{ .expect_stdout_exact = "0.8.0\n" }, 510 | }); 511 | } 512 | 513 | tests.addWithClean(.{ 514 | .name = "test-run-8", 515 | .env = .{ .dir = master_7_and_8, .with_compilers = "0.8.0" }, 516 | .argv = &.{ "run", "0.8.0", "version" }, 517 | .check = .{ .expect_stdout_match = "0.8.0\n" }, 518 | }); 519 | tests.addWithClean(.{ 520 | .name = "test-run-doesnotexist", 521 | .env = .{ .dir = master_7_and_8 }, 522 | .argv = &.{ "run", "doesnotexist", "version" }, 523 | .check = .{ .expect_stderr_match = "error: compiler 'doesnotexist' does not exist, fetch it first with: zigup fetch doesnotexist\n" }, 524 | }); 525 | 526 | tests.addWithClean(.{ 527 | .name = "test-clean-default-master", 528 | .env = .{ .dir = master_7_and_8 }, 529 | .argv = &.{"clean"}, 530 | .checks = &.{ 531 | .{ .expect_stderr_match = "keeping '" }, 532 | .{ .expect_stderr_match = "' (is default compiler)\n" }, 533 | .{ .expect_stderr_match = "deleting '" }, 534 | .{ .expect_stderr_match = "0.7.0'\n" }, 535 | .{ .expect_stderr_match = "0.8.0'\n" }, 536 | .{ .expect_stdout_exact = "" }, 537 | }, 538 | }); 539 | 540 | { 541 | const default7 = tests.add(.{ 542 | .name = "test-set-default-7", 543 | .env = .{ .dir = master_7_and_8 }, 544 | .argv = &.{ "default", "0.7.0" }, 545 | .checks = &.{ 546 | .{ .expect_stdout_exact = "" }, 547 | }, 548 | }); 549 | tests.addWithClean(.{ 550 | .name = "test-clean-default-7", 551 | .env = .{ .dir = default7 }, 552 | .argv = &.{"clean"}, 553 | .checks = &.{ 554 | .{ .expect_stderr_match = "keeping '" }, 555 | .{ .expect_stderr_match = "' (it is master)\n" }, 556 | .{ .expect_stderr_match = "keeping '0.7.0' (is default compiler)\n" }, 557 | .{ .expect_stderr_match = "deleting '" }, 558 | .{ .expect_stderr_match = "0.8.0'\n" }, 559 | .{ .expect_stdout_exact = "" }, 560 | }, 561 | }); 562 | } 563 | 564 | { 565 | const keep8 = tests.add(.{ 566 | .name = "test-keep8", 567 | .env = .{ .dir = master_7_and_8 }, 568 | .argv = &.{ "keep", "0.8.0" }, 569 | .check = .{ .expect_stdout_exact = "" }, 570 | }); 571 | 572 | { 573 | const keep8_default_7 = tests.add(.{ 574 | .name = "test-set-default-7-keep8", 575 | .env = .{ .dir = keep8 }, 576 | .argv = &.{ "default", "0.7.0" }, 577 | .checks = &.{ 578 | .{ .expect_stdout_exact = "" }, 579 | }, 580 | }); 581 | tests.addWithClean(.{ 582 | .name = "test-clean-default-7-keep8", 583 | .env = .{ .dir = keep8_default_7 }, 584 | .argv = &.{"clean"}, 585 | .checks = &.{ 586 | .{ .expect_stderr_match = "keeping '" }, 587 | .{ .expect_stderr_match = "' (it is master)\n" }, 588 | .{ .expect_stderr_match = "keeping '0.7.0' (is default compiler)\n" }, 589 | .{ .expect_stderr_match = "keeping '0.8.0' (has keep file)\n" }, 590 | .{ .expect_stdout_exact = "" }, 591 | }, 592 | }); 593 | tests.addWithClean(.{ 594 | .name = "test-clean-master", 595 | .env = .{ .dir = keep8_default_7 }, 596 | .argv = &.{ "clean", "master" }, 597 | .checks = &.{ 598 | .{ .expect_stderr_match = "deleting '" }, 599 | .{ .expect_stderr_match = "master'\n" }, 600 | .{ .expect_stdout_exact = "" }, 601 | }, 602 | }); 603 | } 604 | 605 | const after_clean = tests.add(.{ 606 | .name = "test-clean-keep8", 607 | .env = .{ .dir = keep8 }, 608 | .argv = &.{"clean"}, 609 | .checks = &.{ 610 | .{ .expect_stderr_match = "keeping '" }, 611 | .{ .expect_stderr_match = "' (is default compiler)\n" }, 612 | .{ .expect_stderr_match = "keeping '0.8.0' (has keep file)\n" }, 613 | .{ .expect_stderr_match = "deleting '" }, 614 | .{ .expect_stderr_match = "0.7.0'\n" }, 615 | }, 616 | }); 617 | 618 | tests.addWithClean(.{ 619 | .name = "test-set-default-7-after-clean", 620 | .env = .{ .dir = after_clean }, 621 | .argv = &.{ "default", "0.7.0" }, 622 | .checks = &.{ 623 | .{ .expect_stderr_match = "error: compiler '0.7.0' is not installed\n" }, 624 | }, 625 | }); 626 | 627 | const default8 = tests.add(.{ 628 | .name = "test-set-default-8-after-clean", 629 | .env = .{ .dir = after_clean }, 630 | .argv = &.{ "default", "0.8.0" }, 631 | .checks = &.{ 632 | .{ .expect_stdout_exact = "" }, 633 | }, 634 | }); 635 | 636 | tests.addWithClean(.{ 637 | .name = "test-clean8-as-default", 638 | .env = .{ .dir = default8 }, 639 | .argv = &.{ "clean", "0.8.0" }, 640 | .checks = &.{ 641 | .{ .expect_stderr_match = "error: cannot clean '0.8.0' (is default compiler)\n" }, 642 | }, 643 | }); 644 | 645 | const after_clean8 = tests.add(.{ 646 | .name = "test-clean8", 647 | .env = .{ .dir = after_clean }, 648 | .argv = &.{ "clean", "0.8.0" }, 649 | .checks = &.{ 650 | .{ .expect_stderr_match = "deleting '" }, 651 | .{ .expect_stderr_match = "0.8.0'\n" }, 652 | .{ .expect_stdout_exact = "" }, 653 | }, 654 | }); 655 | tests.addWithClean(.{ 656 | .name = "test-clean-after-clean8", 657 | .env = .{ .dir = after_clean8 }, 658 | .argv = &.{"clean"}, 659 | .checks = &.{ 660 | .{ .expect_stderr_match = "keeping '" }, 661 | .{ .expect_stderr_match = "' (is default compiler)\n" }, 662 | .{ .expect_stdout_exact = "" }, 663 | }, 664 | }); 665 | } 666 | } 667 | 668 | const native_exe_ext = builtin.os.tag.exeFileExt(builtin.cpu.arch); 669 | 670 | const TestOptions = struct { 671 | name: []const u8, 672 | add_path: bool = true, 673 | env: ?struct { dir: std.Build.LazyPath, with_compilers: []const u8 = "" } = null, 674 | keep_compilers: []const u8 = "", 675 | setup_option: []const u8 = "no-extra-setup", 676 | argv: []const []const u8, 677 | check: ?std.Build.Step.Run.StdIo.Check = null, 678 | checks: []const std.Build.Step.Run.StdIo.Check = &.{}, 679 | }; 680 | 681 | const Tests = struct { 682 | b: *std.Build, 683 | test_step: *std.Build.Step, 684 | zigup_exe: *std.Build.Step.Compile, 685 | runtest_exe: *std.Build.Step.Compile, 686 | shared_options: SharedTestOptions, 687 | 688 | fn addWithClean(tests: Tests, opt: TestOptions) void { 689 | _ = tests.addCommon(opt, .yes_clean); 690 | } 691 | fn add(tests: Tests, opt: TestOptions) std.Build.LazyPath { 692 | return tests.addCommon(opt, .no_clean); 693 | } 694 | 695 | fn compilersArg(arg: []const u8) []const u8 { 696 | return if (arg.len == 0) "--no-compilers" else arg; 697 | } 698 | 699 | fn addCommon(tests: Tests, opt: TestOptions, clean_opt: enum { no_clean, yes_clean }) std.Build.LazyPath { 700 | const b = tests.b; 701 | const run = std.Build.Step.Run.create(b, b.fmt("run {s}", .{opt.name})); 702 | run.failing_to_execute_foreign_is_an_error = tests.shared_options.failing_to_execute_foreign_is_an_error; 703 | run.addArtifactArg(tests.runtest_exe); 704 | run.addArg(opt.name); 705 | run.addArg(if (opt.add_path) "--with-path" else "--no-path"); 706 | if (opt.env) |env| { 707 | run.addDirectoryArg(env.dir); 708 | } else { 709 | run.addArg("--no-input-environment"); 710 | } 711 | run.addArg(compilersArg(if (opt.env) |env| env.with_compilers else "")); 712 | run.addArg(compilersArg(opt.keep_compilers)); 713 | const out_env = run.addOutputDirectoryArg(opt.name); 714 | run.addArg(opt.setup_option); 715 | run.addFileArg(tests.zigup_exe.getEmittedBin()); 716 | run.addArgs(opt.argv); 717 | if (opt.check) |check| { 718 | run.addCheck(check); 719 | } 720 | for (opt.checks) |check| { 721 | run.addCheck(check); 722 | } 723 | 724 | const test_step: *std.Build.Step = switch (clean_opt) { 725 | .no_clean => &run.step, 726 | .yes_clean => &CleanDir.create(tests.b, out_env).step, 727 | }; 728 | 729 | if (tests.shared_options.make_build_steps) { 730 | b.step(opt.name, "").dependOn(test_step); 731 | } 732 | tests.test_step.dependOn(test_step); 733 | 734 | return out_env; 735 | } 736 | }; 737 | 738 | const CleanDir = struct { 739 | step: std.Build.Step, 740 | dir_path: std.Build.LazyPath, 741 | pub fn create(owner: *std.Build, path: std.Build.LazyPath) *CleanDir { 742 | const clean_dir = owner.allocator.create(CleanDir) catch @panic("OOM"); 743 | clean_dir.* = .{ 744 | .step = std.Build.Step.init(.{ 745 | .id = .custom, 746 | .name = owner.fmt("CleanDir {s}", .{path.getDisplayName()}), 747 | .owner = owner, 748 | .makeFn = &make, 749 | }), 750 | .dir_path = path.dupe(owner), 751 | }; 752 | path.addStepDependencies(&clean_dir.step); 753 | return clean_dir; 754 | } 755 | fn make(step: *std.Build.Step, opts: std.Build.Step.MakeOptions) !void { 756 | _ = opts; 757 | const b = step.owner; 758 | const clean_dir: *CleanDir = @fieldParentPtr("step", step); 759 | try b.build_root.handle.deleteTree(clean_dir.dir_path.getPath(b)); 760 | } 761 | }; 762 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zigup, 3 | .version = "0.0.1", 4 | .fingerprint = 0x7cbb96d07979df0f, 5 | .minimum_zig_version = "0.14.0", 6 | 7 | .paths = .{ 8 | "LICENSE", 9 | "README.md", 10 | "build.zig", 11 | "build.zig.zon", 12 | "fixdeletetree.zig", 13 | "runtest.zig", 14 | "win32exelink.zig", 15 | "zigup.zig", 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /fixdeletetree.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | // 5 | // TODO: we should fix std library to address these issues 6 | // 7 | pub fn deleteTree(dir: std.fs.Dir, sub_path: []const u8) !void { 8 | if (builtin.os.tag != .windows) { 9 | return dir.deleteTree(sub_path); 10 | } 11 | 12 | // workaround issue on windows where it just doesn't delete things 13 | const MAX_ATTEMPTS = 10; 14 | var attempt: u8 = 0; 15 | while (true) : (attempt += 1) { 16 | if (dir.deleteTree(sub_path)) { 17 | return; 18 | } else |err| { 19 | if (attempt == MAX_ATTEMPTS) return err; 20 | switch (err) { 21 | error.FileBusy => { 22 | std.log.warn("path '{s}' is busy (attempt {}), will retry", .{ sub_path, attempt }); 23 | std.time.sleep(std.time.ns_per_ms * 100); // sleep for 100 ms 24 | }, 25 | else => |e| return e, 26 | } 27 | } 28 | } 29 | } 30 | pub fn deleteTreeAbsolute(dir_absolute: []const u8) !void { 31 | if (builtin.os.tag != .windows) { 32 | return std.fs.deleteTreeAbsolute(dir_absolute); 33 | } 34 | std.debug.assert(std.fs.path.isAbsolute(dir_absolute)); 35 | return deleteTree(std.fs.cwd(), dir_absolute); 36 | } 37 | -------------------------------------------------------------------------------- /runtest.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | 4 | const fixdeletetree = @import("fixdeletetree.zig"); 5 | 6 | const exe_ext = builtin.os.tag.exeFileExt(builtin.cpu.arch); 7 | 8 | fn compilersArg(arg: []const u8) []const u8 { 9 | return if (std.mem.eql(u8, arg, "--no-compilers")) "" else arg; 10 | } 11 | 12 | pub fn main() !void { 13 | var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); 14 | const arena = arena_instance.allocator(); 15 | const all_args = try std.process.argsAlloc(arena); 16 | if (all_args.len < 7) @panic("not enough cmdline args"); 17 | 18 | const test_name = all_args[1]; 19 | const add_path_option = all_args[2]; 20 | const in_env_dir = all_args[3]; 21 | const with_compilers = compilersArg(all_args[4]); 22 | const keep_compilers = compilersArg(all_args[5]); 23 | const out_env_dir = all_args[6]; 24 | const setup_option = all_args[7]; 25 | const zigup_exe = all_args[8]; 26 | const zigup_args = all_args[9..]; 27 | 28 | const add_path = blk: { 29 | if (std.mem.eql(u8, add_path_option, "--with-path")) break :blk true; 30 | if (std.mem.eql(u8, add_path_option, "--no-path")) break :blk false; 31 | std.log.err("expected '--with-path' or '--no-path' but got '{s}'", .{add_path_option}); 32 | std.process.exit(0xff); 33 | }; 34 | 35 | try fixdeletetree.deleteTree(std.fs.cwd(), out_env_dir); 36 | try std.fs.cwd().makeDir(out_env_dir); 37 | 38 | // make a file named after the test so we can find this directory in the cache 39 | _ = test_name; 40 | // { 41 | // const test_marker_file = try std.fs.path.join(arena, &.{ out_env_dir, test_name}); 42 | // defer arena.free(test_marker_file); 43 | // var file = try std.fs.cwd().createFile(test_marker_file, .{}); 44 | // defer file.close(); 45 | // try file.writer().print("this file marks this directory as the output for test: {s}\n", .{test_name}); 46 | // } 47 | 48 | const appdata = try std.fs.path.join(arena, &.{ out_env_dir, "appdata" }); 49 | const path_link = try std.fs.path.join(arena, &.{ out_env_dir, "zig" ++ exe_ext }); 50 | const install_dir = try std.fs.path.join(arena, &.{ out_env_dir, "install" }); 51 | const install_dir_parsed = switch (parseInstallDir(install_dir)) { 52 | .good => |p| p, 53 | .bad => |reason| std.debug.panic("failed to parse install dir '{s}': {s}", .{ install_dir, reason }), 54 | }; 55 | 56 | const install_dir_setting_path = try std.fs.path.join(arena, &.{ appdata, "install-dir" }); 57 | defer arena.free(install_dir_setting_path); 58 | 59 | if (std.mem.eql(u8, in_env_dir, "--no-input-environment")) { 60 | try std.fs.cwd().makeDir(install_dir); 61 | try std.fs.cwd().makeDir(appdata); 62 | var file = try std.fs.cwd().createFile(install_dir_setting_path, .{}); 63 | defer file.close(); 64 | try file.writer().writeAll(install_dir); 65 | } else { 66 | var shared_sibling_state: SharedSiblingState = .{}; 67 | try copyEnvDir( 68 | arena, 69 | in_env_dir, 70 | out_env_dir, 71 | in_env_dir, 72 | out_env_dir, 73 | .{ .with_compilers = with_compilers }, 74 | &shared_sibling_state, 75 | ); 76 | 77 | const input_install_dir = blk: { 78 | var file = try std.fs.cwd().openFile(install_dir_setting_path, .{}); 79 | defer file.close(); 80 | break :blk try file.readToEndAlloc(arena, std.math.maxInt(usize)); 81 | }; 82 | defer arena.free(input_install_dir); 83 | switch (parseInstallDir(input_install_dir)) { 84 | .good => |input_install_dir_parsed| { 85 | std.debug.assert(std.mem.eql(u8, install_dir_parsed.cache_o, input_install_dir_parsed.cache_o)); 86 | var file = try std.fs.cwd().createFile(install_dir_setting_path, .{}); 87 | defer file.close(); 88 | try file.writer().writeAll(install_dir); 89 | }, 90 | .bad => { 91 | // the install dir must have been customized, keep it 92 | }, 93 | } 94 | } 95 | 96 | var maybe_second_bin_dir: ?[]const u8 = null; 97 | 98 | if (std.mem.eql(u8, setup_option, "no-extra-setup")) { 99 | // nothing extra to setup 100 | } else if (std.mem.eql(u8, setup_option, "path-link-is-directory")) { 101 | std.fs.cwd().deleteFile(path_link) catch |err| switch (err) { 102 | error.FileNotFound => {}, 103 | else => |e| return e, 104 | }; 105 | try std.fs.cwd().makeDir(path_link); 106 | } else if (std.mem.eql(u8, setup_option, "another-zig")) { 107 | maybe_second_bin_dir = try std.fs.path.join(arena, &.{ out_env_dir, "bin2" }); 108 | try std.fs.cwd().makeDir(maybe_second_bin_dir.?); 109 | 110 | const fake_zig = try std.fs.path.join(arena, &.{ 111 | maybe_second_bin_dir.?, 112 | "zig" ++ comptime builtin.target.exeFileExt(), 113 | }); 114 | defer arena.free(fake_zig); 115 | var file = try std.fs.cwd().createFile(fake_zig, .{}); 116 | defer file.close(); 117 | try file.writer().writeAll("a fake executable"); 118 | } else { 119 | std.log.err("unknown setup option '{s}'", .{setup_option}); 120 | std.process.exit(0xff); 121 | } 122 | 123 | var argv = std.ArrayList([]const u8).init(arena); 124 | try argv.append(zigup_exe); 125 | try argv.append("--appdata"); 126 | try argv.append(appdata); 127 | try argv.append("--path-link"); 128 | try argv.append(path_link); 129 | try argv.appendSlice(zigup_args); 130 | 131 | if (true) { 132 | try std.io.getStdErr().writer().writeAll("runtest exec: "); 133 | for (argv.items) |arg| { 134 | try std.io.getStdErr().writer().print(" {s}", .{arg}); 135 | } 136 | try std.io.getStdErr().writer().writeAll("\n"); 137 | } 138 | 139 | var child = std.process.Child.init(argv.items, arena); 140 | 141 | if (add_path) { 142 | var env_map = try std.process.getEnvMap(arena); 143 | // make sure the directory with our path-link comes first in PATH 144 | var new_path = std.ArrayList(u8).init(arena); 145 | if (maybe_second_bin_dir) |second_bin_dir| { 146 | try new_path.appendSlice(second_bin_dir); 147 | try new_path.append(std.fs.path.delimiter); 148 | } 149 | try new_path.appendSlice(out_env_dir); 150 | try new_path.append(std.fs.path.delimiter); 151 | if (env_map.get("PATH")) |path| { 152 | try new_path.appendSlice(path); 153 | } 154 | try env_map.put("PATH", new_path.items); 155 | child.env_map = &env_map; 156 | } else if (maybe_second_bin_dir) |_| @panic("invalid config"); 157 | 158 | try child.spawn(); 159 | const result = try child.wait(); 160 | switch (result) { 161 | .Exited => |c| if (c != 0) std.process.exit(c), 162 | else => |sig| { 163 | std.log.err("zigup terminated from '{s}' with {}", .{ @tagName(result), sig }); 164 | std.process.exit(0xff); 165 | }, 166 | } 167 | 168 | { 169 | var dir = try std.fs.cwd().openDir(install_dir, .{ .iterate = true }); 170 | defer dir.close(); 171 | var it = dir.iterate(); 172 | while (try it.next()) |install_entry| { 173 | switch (install_entry.kind) { 174 | .directory => {}, 175 | else => continue, 176 | } 177 | if (containsCompiler(keep_compilers, install_entry.name)) { 178 | std.log.info("keeping compiler '{s}'", .{install_entry.name}); 179 | continue; 180 | } 181 | const files_path = try std.fs.path.join(arena, &.{ install_entry.name, "files" }); 182 | var files_dir = try dir.openDir(files_path, .{ .iterate = true }); 183 | defer files_dir.close(); 184 | var files_it = files_dir.iterate(); 185 | var is_first = true; 186 | while (try files_it.next()) |files_entry| { 187 | if (is_first) { 188 | std.log.info("cleaning compiler '{s}'", .{install_entry.name}); 189 | is_first = false; 190 | } 191 | try fixdeletetree.deleteTree(files_dir, files_entry.name); 192 | } 193 | } 194 | } 195 | } 196 | 197 | const ParsedInstallDir = struct { 198 | test_name: []const u8, 199 | hash: []const u8, 200 | cache_o: []const u8, 201 | }; 202 | fn parseInstallDir(install_dir: []const u8) union(enum) { 203 | good: ParsedInstallDir, 204 | bad: []const u8, 205 | } { 206 | { 207 | const name = std.fs.path.basename(install_dir); 208 | if (!std.mem.eql(u8, name, "install")) return .{ .bad = "did not end with 'install'" }; 209 | } 210 | const test_dir = std.fs.path.dirname(install_dir) orelse return .{ .bad = "missing test dir" }; 211 | const test_name = std.fs.path.basename(test_dir); 212 | const cache_dir = std.fs.path.dirname(test_dir) orelse return .{ .bad = "missing cache/hash dir" }; 213 | const hash = std.fs.path.basename(cache_dir); 214 | return .{ .good = .{ 215 | .test_name = test_name, 216 | .hash = hash, 217 | .cache_o = std.fs.path.dirname(cache_dir) orelse return .{ .bad = "missing cache o dir" }, 218 | } }; 219 | } 220 | 221 | fn containsCompiler(compilers: []const u8, compiler: []const u8) bool { 222 | var it = std.mem.splitScalar(u8, compilers, ','); 223 | while (it.next()) |c| { 224 | if (std.mem.eql(u8, c, compiler)) return true; 225 | } 226 | return false; 227 | } 228 | 229 | fn isCompilerFilesEntry(path: []const u8) ?[]const u8 { 230 | var it = std.fs.path.NativeComponentIterator.init(path) catch std.debug.panic("invalid path '{s}'", .{path}); 231 | { 232 | const name = (it.next() orelse return null).name; 233 | if (!std.mem.eql(u8, name, "install")) return null; 234 | } 235 | const compiler = it.next() orelse return null; 236 | const leaf = (it.next() orelse return null).name; 237 | if (!std.mem.eql(u8, leaf, "files")) return null; 238 | _ = it.next() orelse return null; 239 | if (null != it.next()) return null; 240 | return compiler.name; 241 | } 242 | 243 | const SharedSiblingState = struct { 244 | logged: bool = false, 245 | }; 246 | fn copyEnvDir( 247 | allocator: std.mem.Allocator, 248 | in_root: []const u8, 249 | out_root: []const u8, 250 | in_path: []const u8, 251 | out_path: []const u8, 252 | opt: struct { with_compilers: []const u8 }, 253 | shared_sibling_state: *SharedSiblingState, 254 | ) !void { 255 | std.debug.assert(std.mem.startsWith(u8, in_path, in_root)); 256 | std.debug.assert(std.mem.startsWith(u8, out_path, out_root)); 257 | 258 | { 259 | const separators = switch (builtin.os.tag) { 260 | .windows => "\\/", 261 | else => "/", 262 | }; 263 | const relative = std.mem.trimLeft(u8, in_path[in_root.len..], separators); 264 | if (isCompilerFilesEntry(relative)) |compiler| { 265 | const exclude = !containsCompiler(opt.with_compilers, compiler); 266 | if (!shared_sibling_state.logged) { 267 | shared_sibling_state.logged = true; 268 | std.log.info("{s} compiler '{s}'", .{ if (exclude) "excluding" else "including", compiler }); 269 | } 270 | if (exclude) return; 271 | } 272 | } 273 | 274 | var in_dir = try std.fs.cwd().openDir(in_path, .{ .iterate = true }); 275 | defer in_dir.close(); 276 | 277 | var it = in_dir.iterate(); 278 | while (try it.next()) |entry| { 279 | const in_sub_path = try std.fs.path.join(allocator, &.{ in_path, entry.name }); 280 | defer allocator.free(in_sub_path); 281 | const out_sub_path = try std.fs.path.join(allocator, &.{ out_path, entry.name }); 282 | defer allocator.free(out_sub_path); 283 | switch (entry.kind) { 284 | .directory => { 285 | try std.fs.cwd().makeDir(out_sub_path); 286 | var shared_child_state: SharedSiblingState = .{}; 287 | try copyEnvDir(allocator, in_root, out_root, in_sub_path, out_sub_path, opt, &shared_child_state); 288 | }, 289 | .file => try std.fs.cwd().copyFile(in_sub_path, std.fs.cwd(), out_sub_path, .{}), 290 | .sym_link => { 291 | var target_buf: [std.fs.max_path_bytes]u8 = undefined; 292 | const in_target = try std.fs.cwd().readLink(in_sub_path, &target_buf); 293 | var out_target_buf: [std.fs.max_path_bytes]u8 = undefined; 294 | const out_target = blk: { 295 | if (std.fs.path.isAbsolute(in_target)) { 296 | if (!std.mem.startsWith(u8, in_target, in_root)) std.debug.panic( 297 | "expected symlink target to start with '{s}' but got '{s}'", 298 | .{ in_root, in_target }, 299 | ); 300 | break :blk try std.fmt.bufPrint( 301 | &out_target_buf, 302 | "{s}{s}", 303 | .{ out_root, in_target[in_root.len..] }, 304 | ); 305 | } 306 | break :blk in_target; 307 | }; 308 | 309 | if (builtin.os.tag == .windows) @panic( 310 | "we got a symlink on windows?", 311 | ) else try std.posix.symlink(out_target, out_sub_path); 312 | }, 313 | else => std.debug.panic("copy {}", .{entry}), 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /unzip.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | 4 | fn oom(e: error{OutOfMemory}) noreturn { 5 | @panic(@errorName(e)); 6 | } 7 | fn fatal(comptime fmt: []const u8, args: anytype) noreturn { 8 | std.log.err(fmt, args); 9 | std.process.exit(0xff); 10 | } 11 | 12 | fn usage() noreturn { 13 | std.io.getStdErr().writer().print("Usage: unzip [-d DIR] ZIP_FILE\n", .{}) catch |e| @panic(@errorName(e)); 14 | std.process.exit(1); 15 | } 16 | 17 | var windows_args_arena = if (builtin.os.tag == .windows) 18 | std.heap.ArenaAllocator.init(std.heap.page_allocator) 19 | else 20 | struct {}{}; 21 | pub fn cmdlineArgs() [][*:0]u8 { 22 | if (builtin.os.tag == .windows) { 23 | const slices = std.process.argsAlloc(windows_args_arena.allocator()) catch |err| switch (err) { 24 | error.OutOfMemory => oom(error.OutOfMemory), 25 | //error.InvalidCmdLine => @panic("InvalidCmdLine"), 26 | error.Overflow => @panic("Overflow while parsing command line"), 27 | }; 28 | const args = windows_args_arena.allocator().alloc([*:0]u8, slices.len - 1) catch |e| oom(e); 29 | for (slices[1..], 0..) |slice, i| { 30 | args[i] = slice.ptr; 31 | } 32 | return args; 33 | } 34 | return std.os.argv.ptr[1..std.os.argv.len]; 35 | } 36 | 37 | pub fn main() !void { 38 | var cmdline_opt: struct { 39 | dir_arg: ?[]u8 = null, 40 | } = .{}; 41 | 42 | const cmd_args = blk: { 43 | const cmd_args = cmdlineArgs(); 44 | var arg_index: usize = 0; 45 | var non_option_len: usize = 0; 46 | while (arg_index < cmd_args.len) : (arg_index += 1) { 47 | const arg = std.mem.span(cmd_args[arg_index]); 48 | if (!std.mem.startsWith(u8, arg, "-")) { 49 | cmd_args[non_option_len] = arg; 50 | non_option_len += 1; 51 | } else if (std.mem.eql(u8, arg, "-d")) { 52 | arg_index += 1; 53 | if (arg_index == cmd_args.len) 54 | fatal("option '{s}' requires an argument", .{arg}); 55 | cmdline_opt.dir_arg = std.mem.span(cmd_args[arg_index]); 56 | } else { 57 | fatal("unknown cmdline option '{s}'", .{arg}); 58 | } 59 | } 60 | break :blk cmd_args[0..non_option_len]; 61 | }; 62 | 63 | if (cmd_args.len != 1) usage(); 64 | const zip_file_arg = std.mem.span(cmd_args[0]); 65 | 66 | var out_dir = blk: { 67 | if (cmdline_opt.dir_arg) |dir| { 68 | break :blk std.fs.cwd().openDir(dir, .{}) catch |err| switch (err) { 69 | error.FileNotFound => { 70 | try std.fs.cwd().makePath(dir); 71 | break :blk try std.fs.cwd().openDir(dir, .{}); 72 | }, 73 | else => fatal("failed to open output directory '{s}' with {s}", .{ dir, @errorName(err) }), 74 | }; 75 | } 76 | break :blk std.fs.cwd(); 77 | }; 78 | defer if (cmdline_opt.dir_arg) |_| out_dir.close(); 79 | 80 | const zip_file = std.fs.cwd().openFile(zip_file_arg, .{}) catch |err| 81 | fatal("open '{s}' failed: {s}", .{ zip_file_arg, @errorName(err) }); 82 | defer zip_file.close(); 83 | try std.zip.extract(out_dir, zip_file.seekableStream(), .{ 84 | .allow_backslashes = true, 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /win32exelink.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | 4 | const log = std.log.scoped(.zigexelink); 5 | 6 | // NOTE: to prevent the exe from having multiple markers, I can't create a separate string literal 7 | // for the marker and get the length from that, I have to hardcode the length 8 | const exe_marker_len = 42; 9 | 10 | // I'm exporting this and making it mutable to make sure the compiler keeps it around 11 | // and prevent it from evaluting its contents at comptime 12 | export var zig_exe_string: [exe_marker_len + std.fs.max_path_bytes + 1]u8 = 13 | ("!!!THIS MARKS THE zig_exe_string MEMORY!!#" ++ ([1]u8{0} ** (std.fs.max_path_bytes + 1))).*; 14 | 15 | const global = struct { 16 | var child: std.process.Child = undefined; 17 | var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); 18 | const arena = arena_instance.allocator(); 19 | }; 20 | 21 | pub fn main() !u8 { 22 | // Sanity check that the exe_marker_len is right (note: not fullproof) 23 | std.debug.assert(zig_exe_string[exe_marker_len - 1] == '#'); 24 | if (zig_exe_string[exe_marker_len] == 0) { 25 | log.err("the zig target executable has not been set in the exelink", .{}); 26 | return 0xff; // fail 27 | } 28 | var zig_exe_len: usize = 1; 29 | while (zig_exe_string[exe_marker_len + zig_exe_len] != 0) { 30 | zig_exe_len += 1; 31 | if (exe_marker_len + zig_exe_len > std.fs.max_path_bytes) { 32 | log.err("the zig target execuable is either too big (over {}) or the exe is corrupt", .{std.fs.max_path_bytes}); 33 | return 1; 34 | } 35 | } 36 | const zig_exe = zig_exe_string[exe_marker_len .. exe_marker_len + zig_exe_len :0]; 37 | 38 | const args = try std.process.argsAlloc(global.arena); 39 | if (args.len >= 2 and std.mem.eql(u8, args[1], "exelink")) { 40 | try std.io.getStdOut().writer().writeAll(zig_exe); 41 | return 0; 42 | } 43 | args[0] = zig_exe; 44 | 45 | // NOTE: create the process.child before calling SetConsoleCtrlHandler because it uses it 46 | global.child = std.process.Child.init(args, global.arena); 47 | 48 | if (0 == win32.SetConsoleCtrlHandler(consoleCtrlHandler, 1)) { 49 | log.err("SetConsoleCtrlHandler failed, error={}", .{@intFromEnum(win32.GetLastError())}); 50 | return 0xff; // fail 51 | } 52 | 53 | try global.child.spawn(); 54 | return switch (try global.child.wait()) { 55 | .Exited => |e| e, 56 | .Signal => 0xff, 57 | .Stopped => 0xff, 58 | .Unknown => 0xff, 59 | }; 60 | } 61 | 62 | fn consoleCtrlHandler(ctrl_type: u32) callconv(@import("std").os.windows.WINAPI) win32.BOOL { 63 | // 64 | // NOTE: Do I need to synchronize this with the main thread? 65 | // 66 | const name: []const u8 = switch (ctrl_type) { 67 | win32.CTRL_C_EVENT => "Control-C", 68 | win32.CTRL_BREAK_EVENT => "Break", 69 | win32.CTRL_CLOSE_EVENT => "Close", 70 | win32.CTRL_LOGOFF_EVENT => "Logoff", 71 | win32.CTRL_SHUTDOWN_EVENT => "Shutdown", 72 | else => "Unknown", 73 | }; 74 | // TODO: should we stop the process on a break event? 75 | log.info("caught ctrl signal {d} ({s}), stopping process...", .{ ctrl_type, name }); 76 | const exit_code = switch (global.child.kill() catch |err| { 77 | log.err("failed to kill process, error={s}", .{@errorName(err)}); 78 | std.process.exit(0xff); 79 | }) { 80 | .Exited => |e| e, 81 | .Signal => 0xff, 82 | .Stopped => 0xff, 83 | .Unknown => 0xff, 84 | }; 85 | std.process.exit(exit_code); 86 | unreachable; 87 | } 88 | 89 | const win32 = struct { 90 | pub const BOOL = i32; 91 | pub const CTRL_C_EVENT = @as(u32, 0); 92 | pub const CTRL_BREAK_EVENT = @as(u32, 1); 93 | pub const CTRL_CLOSE_EVENT = @as(u32, 2); 94 | pub const CTRL_LOGOFF_EVENT = @as(u32, 5); 95 | pub const CTRL_SHUTDOWN_EVENT = @as(u32, 6); 96 | pub const GetLastError = std.os.windows.kernel32.GetLastError; 97 | pub const PHANDLER_ROUTINE = switch (builtin.zig_backend) { 98 | .stage1 => fn ( 99 | CtrlType: u32, 100 | ) callconv(@import("std").os.windows.WINAPI) BOOL, 101 | else => *const fn ( 102 | CtrlType: u32, 103 | ) callconv(@import("std").os.windows.WINAPI) BOOL, 104 | }; 105 | pub extern "kernel32" fn SetConsoleCtrlHandler( 106 | HandlerRoutine: ?PHANDLER_ROUTINE, 107 | Add: BOOL, 108 | ) callconv(@import("std").os.windows.WINAPI) BOOL; 109 | }; 110 | -------------------------------------------------------------------------------- /zigup.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const mem = std.mem; 4 | 5 | const ArrayList = std.ArrayList; 6 | const Allocator = mem.Allocator; 7 | 8 | const fixdeletetree = @import("fixdeletetree.zig"); 9 | 10 | const arch = switch (builtin.cpu.arch) { 11 | .aarch64 => "aarch64", 12 | .arm => "armv7a", 13 | .powerpc64le => "powerpc64le", 14 | .riscv64 => "riscv64", 15 | .s390x => "s390x", 16 | .x86 => "x86", 17 | .x86_64 => "x86_64", 18 | else => @compileError("Unsupported CPU Architecture"), 19 | }; 20 | const os = switch (builtin.os.tag) { 21 | .linux => "linux", 22 | .macos => "macos", 23 | .windows => "windows", 24 | else => @compileError("Unsupported OS"), 25 | }; 26 | const os_arch = os ++ "-" ++ arch; 27 | const arch_os = arch ++ "-" ++ os; 28 | const archive_ext = if (builtin.os.tag == .windows) "zip" else "tar.xz"; 29 | 30 | var global_override_appdata: ?[]const u8 = null; // only used for testing 31 | var global_optional_install_dir: ?[]const u8 = null; 32 | var global_optional_path_link: ?[]const u8 = null; 33 | 34 | var global_enable_log = true; 35 | fn loginfo(comptime fmt: []const u8, args: anytype) void { 36 | if (global_enable_log) { 37 | std.debug.print(fmt ++ "\n", args); 38 | } 39 | } 40 | 41 | pub fn oom(e: error{OutOfMemory}) noreturn { 42 | @panic(@errorName(e)); 43 | } 44 | 45 | const DownloadResult = union(enum) { 46 | ok: void, 47 | err: []u8, 48 | pub fn deinit(self: DownloadResult, allocator: Allocator) void { 49 | switch (self) { 50 | .ok => {}, 51 | .err => |e| allocator.free(e), 52 | } 53 | } 54 | }; 55 | fn download(allocator: Allocator, url: []const u8, writer: anytype) DownloadResult { 56 | const uri = std.Uri.parse(url) catch |err| return .{ .err = std.fmt.allocPrint( 57 | allocator, 58 | "the URL is invalid ({s})", 59 | .{@errorName(err)}, 60 | ) catch |e| oom(e) }; 61 | 62 | var client = std.http.Client{ .allocator = allocator }; 63 | defer client.deinit(); 64 | 65 | client.initDefaultProxies(allocator) catch |err| return .{ .err = std.fmt.allocPrint( 66 | allocator, 67 | "failed to query the HTTP proxy settings with {s}", 68 | .{@errorName(err)}, 69 | ) catch |e| oom(e) }; 70 | 71 | var header_buffer: [4096]u8 = undefined; 72 | var request = client.open(.GET, uri, .{ 73 | .server_header_buffer = &header_buffer, 74 | .keep_alive = false, 75 | }) catch |err| return .{ .err = std.fmt.allocPrint( 76 | allocator, 77 | "failed to connect to the HTTP server with {s}", 78 | .{@errorName(err)}, 79 | ) catch |e| oom(e) }; 80 | 81 | defer request.deinit(); 82 | 83 | request.send() catch |err| return .{ .err = std.fmt.allocPrint( 84 | allocator, 85 | "failed to send the HTTP request with {s}", 86 | .{@errorName(err)}, 87 | ) catch |e| oom(e) }; 88 | request.wait() catch |err| return .{ .err = std.fmt.allocPrint( 89 | allocator, 90 | "failed to read the HTTP response headers with {s}", 91 | .{@errorName(err)}, 92 | ) catch |e| oom(e) }; 93 | 94 | if (request.response.status != .ok) return .{ .err = std.fmt.allocPrint( 95 | allocator, 96 | "the HTTP server replied with unsuccessful response '{d} {s}'", 97 | .{ @intFromEnum(request.response.status), request.response.status.phrase() orelse "" }, 98 | ) catch |e| oom(e) }; 99 | 100 | // TODO: we take advantage of request.response.content_length 101 | 102 | var buf: [4096]u8 = undefined; 103 | while (true) { 104 | const len = request.reader().read(&buf) catch |err| return .{ .err = std.fmt.allocPrint( 105 | allocator, 106 | "failed to read the HTTP response body with {s}'", 107 | .{@errorName(err)}, 108 | ) catch |e| oom(e) }; 109 | if (len == 0) 110 | return .ok; 111 | writer.writeAll(buf[0..len]) catch |err| return .{ .err = std.fmt.allocPrint( 112 | allocator, 113 | "failed to write the HTTP response body with {s}'", 114 | .{@errorName(err)}, 115 | ) catch |e| oom(e) }; 116 | } 117 | } 118 | 119 | const DownloadStringResult = union(enum) { 120 | ok: []u8, 121 | err: []u8, 122 | }; 123 | fn downloadToString(allocator: Allocator, url: []const u8) DownloadStringResult { 124 | var response_array_list = ArrayList(u8).initCapacity(allocator, 50 * 1024) catch |e| oom(e); // 50 KB (modify if response is expected to be bigger) 125 | defer response_array_list.deinit(); 126 | switch (download(allocator, url, response_array_list.writer())) { 127 | .ok => return .{ .ok = response_array_list.toOwnedSlice() catch |e| oom(e) }, 128 | .err => |e| return .{ .err = e }, 129 | } 130 | } 131 | 132 | fn ignoreHttpCallback(request: []const u8) void { 133 | _ = request; 134 | } 135 | 136 | fn allocInstallDirStringXdg(allocator: Allocator) error{AlreadyReported}![]const u8 { 137 | // see https://specifications.freedesktop.org/basedir-spec/latest/#variables 138 | // try $XDG_DATA_HOME/zigup first 139 | xdg_var: { 140 | const xdg_data_home = std.posix.getenv("XDG_DATA_HOME") orelse break :xdg_var; 141 | if (xdg_data_home.len == 0) break :xdg_var; 142 | if (!std.fs.path.isAbsolute(xdg_data_home)) { 143 | std.log.err("$XDG_DATA_HOME environment variable '{s}' is not an absolute path", .{xdg_data_home}); 144 | return error.AlreadyReported; 145 | } 146 | return std.fs.path.join(allocator, &[_][]const u8{ xdg_data_home, "zigup" }) catch |e| oom(e); 147 | } 148 | // .. then fallback to $HOME/.local/share/zigup 149 | const home = std.posix.getenv("HOME") orelse { 150 | std.log.err("cannot find install directory, neither $HOME nor $XDG_DATA_HOME environment variables are set", .{}); 151 | return error.AlreadyReported; 152 | }; 153 | if (!std.fs.path.isAbsolute(home)) { 154 | std.log.err("$HOME environment variable '{s}' is not an absolute path", .{home}); 155 | return error.AlreadyReported; 156 | } 157 | return std.fs.path.join(allocator, &[_][]const u8{ home, ".local", "share", "zigup" }) catch |e| oom(e); 158 | } 159 | 160 | fn getSettingsDir(allocator: Allocator) ?[]const u8 { 161 | const appdata: ?[]const u8 = std.fs.getAppDataDir(allocator, "zigup") catch |err| switch (err) { 162 | error.OutOfMemory => |e| oom(e), 163 | error.AppDataDirUnavailable => null, 164 | }; 165 | // just used for testing, but note we still test getting the builtin appdata dir either way 166 | if (global_override_appdata) |appdata_override| { 167 | if (appdata) |a| allocator.free(a); 168 | return allocator.dupe(u8, appdata_override) catch |e| oom(e); 169 | } 170 | return appdata; 171 | } 172 | 173 | fn readInstallDir(allocator: Allocator) !?[]const u8 { 174 | const settings_dir_path = getSettingsDir(allocator) orelse return null; 175 | defer allocator.free(settings_dir_path); 176 | const setting_path = std.fs.path.join(allocator, &.{ settings_dir_path, "install-dir" }) catch |e| oom(e); 177 | defer allocator.free(setting_path); 178 | const content = blk: { 179 | var file = std.fs.cwd().openFile(setting_path, .{}) catch |err| switch (err) { 180 | error.FileNotFound => return null, 181 | else => |e| { 182 | std.log.err("open '{s}' failed with {s}", .{ setting_path, @errorName(e) }); 183 | return error.AlreadyReported; 184 | }, 185 | }; 186 | defer file.close(); 187 | break :blk file.readToEndAlloc(allocator, 9999) catch |err| { 188 | std.log.err("read install dir from '{s}' failed with {s}", .{ setting_path, @errorName(err) }); 189 | return error.AlreadyReported; 190 | }; 191 | }; 192 | errdefer allocator.free(content); 193 | const stripped = std.mem.trimRight(u8, content, " \r\n"); 194 | 195 | if (!std.fs.path.isAbsolute(stripped)) { 196 | std.log.err("install directory '{s}' is not an absolute path, fix this by running `zigup set-install-dir`", .{stripped}); 197 | return error.BadInstallDirSetting; 198 | } 199 | 200 | return allocator.realloc(content, stripped.len) catch |e| oom(e); 201 | } 202 | 203 | fn saveInstallDir(allocator: Allocator, maybe_dir: ?[]const u8) !void { 204 | const settings_dir_path = getSettingsDir(allocator) orelse { 205 | std.log.err("cannot save install dir, unable to find a suitable settings directory", .{}); 206 | return error.AlreadyReported; 207 | }; 208 | defer allocator.free(settings_dir_path); 209 | const setting_path = std.fs.path.join(allocator, &.{ settings_dir_path, "install-dir" }) catch |e| oom(e); 210 | defer allocator.free(setting_path); 211 | if (maybe_dir) |d| { 212 | if (std.fs.path.dirname(setting_path)) |dir| try std.fs.cwd().makePath(dir); 213 | 214 | { 215 | const file = try std.fs.cwd().createFile(setting_path, .{}); 216 | defer file.close(); 217 | try file.writer().writeAll(d); 218 | } 219 | 220 | // sanity check, read it back 221 | const readback = (try readInstallDir(allocator)) orelse { 222 | std.log.err("unable to readback install-dir after saving it", .{}); 223 | return error.AlreadyReported; 224 | }; 225 | defer allocator.free(readback); 226 | if (!std.mem.eql(u8, readback, d)) { 227 | std.log.err("saved install dir readback mismatch\nwrote: '{s}'\nread : '{s}'\n", .{ d, readback }); 228 | return error.AlreadyReported; 229 | } 230 | } else { 231 | std.fs.cwd().deleteFile(setting_path) catch |err| switch (err) { 232 | error.FileNotFound => {}, 233 | else => |e| return e, 234 | }; 235 | } 236 | } 237 | 238 | fn getBuiltinInstallDir(allocator: Allocator) error{AlreadyReported}![]const u8 { 239 | if (builtin.os.tag == .windows) { 240 | const self_exe_dir = std.fs.selfExeDirPathAlloc(allocator) catch |e| { 241 | std.log.err("failed to get exe dir path with {s}", .{@errorName(e)}); 242 | return error.AlreadyReported; 243 | }; 244 | defer allocator.free(self_exe_dir); 245 | return std.fs.path.join(allocator, &.{ self_exe_dir, "zig" }) catch |e| oom(e); 246 | } 247 | return allocInstallDirStringXdg(allocator); 248 | } 249 | 250 | fn allocInstallDirString(allocator: Allocator) error{ AlreadyReported, BadInstallDirSetting }![]const u8 { 251 | if (try readInstallDir(allocator)) |d| return d; 252 | return try getBuiltinInstallDir(allocator); 253 | } 254 | const GetInstallDirOptions = struct { 255 | create: bool, 256 | log: bool = true, 257 | }; 258 | fn getInstallDir(allocator: Allocator, options: GetInstallDirOptions) ![]const u8 { 259 | var optional_dir_to_free_on_error: ?[]const u8 = null; 260 | errdefer if (optional_dir_to_free_on_error) |dir| allocator.free(dir); 261 | 262 | const install_dir = init: { 263 | if (global_optional_install_dir) |dir| break :init dir; 264 | optional_dir_to_free_on_error = try allocInstallDirString(allocator); 265 | break :init optional_dir_to_free_on_error.?; 266 | }; 267 | std.debug.assert(std.fs.path.isAbsolute(install_dir)); 268 | if (options.log) { 269 | loginfo("install directory '{s}'", .{install_dir}); 270 | } 271 | if (options.create) { 272 | loggyMakePath(install_dir) catch |e| switch (e) { 273 | error.PathAlreadyExists => {}, 274 | else => return e, 275 | }; 276 | } 277 | return install_dir; 278 | } 279 | 280 | fn makeZigPathLinkString(allocator: Allocator) ![]const u8 { 281 | if (global_optional_path_link) |path| return path; 282 | 283 | const zigup_dir = try std.fs.selfExeDirPathAlloc(allocator); 284 | defer allocator.free(zigup_dir); 285 | 286 | return try std.fs.path.join(allocator, &[_][]const u8{ zigup_dir, comptime "zig" ++ builtin.target.exeFileExt() }); 287 | } 288 | 289 | // TODO: this should be in standard lib 290 | fn toAbsolute(allocator: Allocator, path: []const u8) ![]u8 { 291 | std.debug.assert(!std.fs.path.isAbsolute(path)); 292 | const cwd = try std.process.getCwdAlloc(allocator); 293 | defer allocator.free(cwd); 294 | return std.fs.path.join(allocator, &[_][]const u8{ cwd, path }); 295 | } 296 | 297 | fn help(allocator: Allocator) !void { 298 | const builtin_install_dir = getBuiltinInstallDir(allocator) catch |err| switch (err) { 299 | error.AlreadyReported => "unknown (see error printed above)", 300 | }; 301 | const current_install_dir = allocInstallDirString(allocator) catch |err| switch (err) { 302 | error.AlreadyReported => "unknown (see error printed above)", 303 | error.BadInstallDirSetting => "invalid (fix with zigup set-install-dir)", 304 | }; 305 | const setting_file: []const u8 = blk: { 306 | if (getSettingsDir(allocator)) |d| break :blk std.fs.path.join(allocator, &.{ d, "install-dir" }) catch |e| oom(e); 307 | break :blk "unavailable"; 308 | }; 309 | 310 | try std.io.getStdErr().writer().print( 311 | \\Download and manage zig compilers. 312 | \\ 313 | \\Common Usage: 314 | \\ 315 | \\ zigup VERSION download and set VERSION compiler as default 316 | \\ zigup fetch VERSION download VERSION compiler 317 | \\ zigup default [VERSION] get or set the default compiler 318 | \\ zigup list list installed compiler versions 319 | \\ zigup clean [VERSION] deletes the given compiler version, otherwise, cleans all compilers 320 | \\ that aren't the default, master, or marked to keep. 321 | \\ zigup keep VERSION mark a compiler to be kept during clean 322 | \\ zigup run VERSION ARGS... run the given VERSION of the compiler with the given ARGS... 323 | \\ 324 | \\ zigup get-install-dir prints the install directory to stdout 325 | \\ zigup set-install-dir [PATH] set the default install directory, omitting the PATH reverts to the builtin default 326 | \\ current default: {s} 327 | \\ setting file : {s} 328 | \\ builtin default: {s} 329 | \\ 330 | \\Uncommon Usage: 331 | \\ 332 | \\ zigup fetch-index download and print the download index json 333 | \\ 334 | \\Common Options: 335 | \\ --install-dir DIR override the default install location 336 | \\ --path-link PATH path to the `zig` symlink that points to the default compiler 337 | \\ this will typically be a file path within a PATH directory so 338 | \\ that the user can just run `zig` 339 | \\ --index override the default index URL that zig versions/URLs are fetched from. 340 | \\ default: 341 | ++ " " ++ default_index_url ++ 342 | \\ 343 | \\ 344 | , 345 | .{ 346 | current_install_dir, 347 | setting_file, 348 | builtin_install_dir, 349 | }, 350 | ); 351 | } 352 | 353 | fn getCmdOpt(args: [][:0]u8, i: *usize) ![]const u8 { 354 | i.* += 1; 355 | if (i.* == args.len) { 356 | std.log.err("option '{s}' requires an argument", .{args[i.* - 1]}); 357 | return error.AlreadyReported; 358 | } 359 | return args[i.*]; 360 | } 361 | 362 | pub fn main() !u8 { 363 | return main2() catch |e| switch (e) { 364 | error.AlreadyReported => return 1, 365 | else => return e, 366 | }; 367 | } 368 | pub fn main2() !u8 { 369 | if (builtin.os.tag == .windows) { 370 | _ = try std.os.windows.WSAStartup(2, 2); 371 | } 372 | 373 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 374 | const allocator = arena.allocator(); 375 | 376 | const args_array = try std.process.argsAlloc(allocator); 377 | // no need to free, os will do it 378 | //defer std.process.argsFree(allocator, argsArray); 379 | 380 | var args = if (args_array.len == 0) args_array else args_array[1..]; 381 | // parse common options 382 | 383 | var index_url: []const u8 = default_index_url; 384 | 385 | { 386 | var i: usize = 0; 387 | var newlen: usize = 0; 388 | while (i < args.len) : (i += 1) { 389 | const arg = args[i]; 390 | if (std.mem.eql(u8, "--install-dir", arg)) { 391 | global_optional_install_dir = try getCmdOpt(args, &i); 392 | if (!std.fs.path.isAbsolute(global_optional_install_dir.?)) { 393 | global_optional_install_dir = try toAbsolute(allocator, global_optional_install_dir.?); 394 | } 395 | } else if (std.mem.eql(u8, "--path-link", arg)) { 396 | global_optional_path_link = try getCmdOpt(args, &i); 397 | if (!std.fs.path.isAbsolute(global_optional_path_link.?)) { 398 | global_optional_path_link = try toAbsolute(allocator, global_optional_path_link.?); 399 | } 400 | } else if (std.mem.eql(u8, "--index", arg)) { 401 | index_url = try getCmdOpt(args, &i); 402 | } else if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) { 403 | try help(allocator); 404 | return 0; 405 | } else if (std.mem.eql(u8, "--appdata", arg)) { 406 | // NOTE: this is a private option just used for testing 407 | global_override_appdata = try getCmdOpt(args, &i); 408 | } else { 409 | if (newlen == 0 and std.mem.eql(u8, "run", arg)) { 410 | return try runCompiler(allocator, args[i + 1 ..]); 411 | } 412 | args[newlen] = args[i]; 413 | newlen += 1; 414 | } 415 | } 416 | args = args[0..newlen]; 417 | } 418 | if (args.len == 0) { 419 | try help(allocator); 420 | return 1; 421 | } 422 | if (std.mem.eql(u8, "get-install-dir", args[0])) { 423 | if (args.len != 1) { 424 | std.log.err("get-install-dir does not accept any cmdline arguments", .{}); 425 | return 1; 426 | } 427 | const install_dir = getInstallDir(allocator, .{ .create = false, .log = false }) catch |err| switch (err) { 428 | error.AlreadyReported => return 1, 429 | else => |e| return e, 430 | }; 431 | try std.io.getStdOut().writer().writeAll(install_dir); 432 | try std.io.getStdOut().writer().writeAll("\n"); 433 | return 0; 434 | } 435 | if (std.mem.eql(u8, "set-install-dir", args[0])) { 436 | const set_args = args[1..]; 437 | switch (set_args.len) { 438 | 0 => try saveInstallDir(allocator, null), 439 | 1 => { 440 | const path = set_args[0]; 441 | if (!std.fs.path.isAbsolute(path)) { 442 | std.log.err("set-install-dir requires an absolute path", .{}); 443 | return 1; 444 | } 445 | try saveInstallDir(allocator, path); 446 | }, 447 | else => |set_arg_count| { 448 | std.log.err("set-install-dir requires 0 or 1 cmdline arg but got {}", .{set_arg_count}); 449 | return 1; 450 | }, 451 | } 452 | return 0; 453 | } 454 | if (std.mem.eql(u8, "fetch-index", args[0])) { 455 | if (args.len != 1) { 456 | std.log.err("'index' command requires 0 arguments but got {d}", .{args.len - 1}); 457 | return 1; 458 | } 459 | var download_index = try fetchDownloadIndex(allocator, index_url); 460 | defer download_index.deinit(allocator); 461 | try std.io.getStdOut().writeAll(download_index.text); 462 | return 0; 463 | } 464 | if (std.mem.eql(u8, "fetch", args[0])) { 465 | if (args.len != 2) { 466 | std.log.err("'fetch' command requires 1 argument but got {d}", .{args.len - 1}); 467 | return 1; 468 | } 469 | try fetchCompiler(allocator, index_url, args[1], .leave_default); 470 | return 0; 471 | } 472 | if (std.mem.eql(u8, "clean", args[0])) { 473 | if (args.len == 1) { 474 | try cleanCompilers(allocator, null); 475 | } else if (args.len == 2) { 476 | try cleanCompilers(allocator, args[1]); 477 | } else { 478 | std.log.err("'clean' command requires 0 or 1 arguments but got {d}", .{args.len - 1}); 479 | return 1; 480 | } 481 | return 0; 482 | } 483 | if (std.mem.eql(u8, "keep", args[0])) { 484 | if (args.len != 2) { 485 | std.log.err("'keep' command requires 1 argument but got {d}", .{args.len - 1}); 486 | return 1; 487 | } 488 | try keepCompiler(allocator, args[1]); 489 | return 0; 490 | } 491 | if (std.mem.eql(u8, "list", args[0])) { 492 | if (args.len != 1) { 493 | std.log.err("'list' command requires 0 arguments but got {d}", .{args.len - 1}); 494 | return 1; 495 | } 496 | try listCompilers(allocator); 497 | return 0; 498 | } 499 | if (std.mem.eql(u8, "default", args[0])) { 500 | if (args.len == 1) { 501 | try printDefaultCompiler(allocator); 502 | return 0; 503 | } 504 | if (args.len == 2) { 505 | const version_string = args[1]; 506 | const install_dir_string = try getInstallDir(allocator, .{ .create = true }); 507 | defer allocator.free(install_dir_string); 508 | const resolved_version_string = init_resolved: { 509 | if (!std.mem.eql(u8, version_string, "master")) 510 | break :init_resolved version_string; 511 | 512 | const optional_master_dir: ?[]const u8 = blk: { 513 | var install_dir = std.fs.openDirAbsolute(install_dir_string, .{ .iterate = true }) catch |e| switch (e) { 514 | error.FileNotFound => break :blk null, 515 | else => return e, 516 | }; 517 | defer install_dir.close(); 518 | break :blk try getMasterDir(allocator, &install_dir); 519 | }; 520 | // no need to free master_dir, this is a short lived program 521 | break :init_resolved optional_master_dir orelse { 522 | std.log.err("master has not been fetched", .{}); 523 | return 1; 524 | }; 525 | }; 526 | const compiler_dir = try std.fs.path.join(allocator, &[_][]const u8{ install_dir_string, resolved_version_string }); 527 | defer allocator.free(compiler_dir); 528 | try setDefaultCompiler(allocator, compiler_dir, .verify_existence); 529 | return 0; 530 | } 531 | std.log.err("'default' command requires 1 or 2 arguments but got {d}", .{args.len - 1}); 532 | return 1; 533 | } 534 | if (args.len == 1) { 535 | try fetchCompiler(allocator, index_url, args[0], .set_default); 536 | return 0; 537 | } 538 | const command = args[0]; 539 | args = args[1..]; 540 | std.log.err("command not impl '{s}'", .{command}); 541 | return 1; 542 | 543 | //const optionalInstallPath = try find_zigs(allocator); 544 | } 545 | 546 | pub fn runCompiler(allocator: Allocator, args: []const []const u8) !u8 { 547 | // disable log so we don't add extra output to whatever the compiler will output 548 | global_enable_log = false; 549 | if (args.len <= 1) { 550 | std.log.err("zigup run requires at least 2 arguments: zigup run VERSION PROG ARGS...", .{}); 551 | return 1; 552 | } 553 | const version_string = args[0]; 554 | const install_dir_string = try getInstallDir(allocator, .{ .create = true }); 555 | defer allocator.free(install_dir_string); 556 | 557 | const compiler_dir = try std.fs.path.join(allocator, &[_][]const u8{ install_dir_string, version_string }); 558 | defer allocator.free(compiler_dir); 559 | if (!try existsAbsolute(compiler_dir)) { 560 | std.log.err("compiler '{s}' does not exist, fetch it first with: zigup fetch {0s}", .{version_string}); 561 | return 1; 562 | } 563 | 564 | var argv = std.ArrayList([]const u8).init(allocator); 565 | try argv.append(try std.fs.path.join(allocator, &.{ compiler_dir, "files", comptime "zig" ++ builtin.target.exeFileExt() })); 566 | try argv.appendSlice(args[1..]); 567 | 568 | // TODO: use "execve" if on linux 569 | var proc = std.process.Child.init(argv.items, allocator); 570 | const ret_val = try proc.spawnAndWait(); 571 | switch (ret_val) { 572 | .Exited => |code| return code, 573 | else => |result| { 574 | std.log.err("compiler exited with {}", .{result}); 575 | return 0xff; 576 | }, 577 | } 578 | } 579 | 580 | const SetDefault = enum { set_default, leave_default }; 581 | 582 | fn fetchCompiler( 583 | allocator: Allocator, 584 | index_url: []const u8, 585 | version_arg: []const u8, 586 | set_default: SetDefault, 587 | ) !void { 588 | const install_dir = try getInstallDir(allocator, .{ .create = true }); 589 | defer allocator.free(install_dir); 590 | 591 | var optional_download_index: ?DownloadIndex = null; 592 | // This is causing an LLVM error 593 | //defer if (optionalDownloadIndex) |_| optionalDownloadIndex.?.deinit(allocator); 594 | // Also I would rather do this, but it doesn't work because of const issues 595 | //defer if (optionalDownloadIndex) |downloadIndex| downloadIndex.deinit(allocator); 596 | 597 | const VersionUrl = struct { version: []const u8, url: []const u8 }; 598 | 599 | // NOTE: we only fetch the download index if the user wants to download 'master', we can skip 600 | // this step for all other versions because the version to URL mapping is fixed (see getDefaultUrl) 601 | const is_master = std.mem.eql(u8, version_arg, "master"); 602 | const version_url = blk: { 603 | // For default index_url we can build the url so we avoid downloading the index 604 | if (!is_master and std.mem.eql(u8, default_index_url, index_url)) 605 | break :blk VersionUrl{ .version = version_arg, .url = try getDefaultUrl(allocator, version_arg) }; 606 | optional_download_index = try fetchDownloadIndex(allocator, index_url); 607 | const master = optional_download_index.?.json.value.object.get(version_arg).?; 608 | const compiler_version = master.object.get("version").?.string; 609 | const master_linux = master.object.get(arch_os).?; 610 | const master_linux_tarball = master_linux.object.get("tarball").?.string; 611 | break :blk VersionUrl{ .version = compiler_version, .url = master_linux_tarball }; 612 | }; 613 | const compiler_dir = try std.fs.path.join(allocator, &[_][]const u8{ install_dir, version_url.version }); 614 | defer allocator.free(compiler_dir); 615 | try installCompiler(allocator, compiler_dir, version_url.url); 616 | if (is_master) { 617 | const master_symlink = try std.fs.path.join(allocator, &[_][]const u8{ install_dir, "master" }); 618 | defer allocator.free(master_symlink); 619 | if (builtin.os.tag == .windows) { 620 | var file = try std.fs.createFileAbsolute(master_symlink, .{}); 621 | defer file.close(); 622 | try file.writer().writeAll(version_url.version); 623 | } else { 624 | _ = try loggyUpdateSymlink(version_url.version, master_symlink, .{ .is_directory = true }); 625 | } 626 | } 627 | if (set_default == .set_default) { 628 | try setDefaultCompiler(allocator, compiler_dir, .existence_verified); 629 | } 630 | } 631 | 632 | const default_index_url = "https://ziglang.org/download/index.json"; 633 | 634 | const DownloadIndex = struct { 635 | text: []u8, 636 | json: std.json.Parsed(std.json.Value), 637 | pub fn deinit(self: *DownloadIndex, allocator: Allocator) void { 638 | self.json.deinit(); 639 | allocator.free(self.text); 640 | } 641 | }; 642 | 643 | fn fetchDownloadIndex(allocator: Allocator, index_url: []const u8) !DownloadIndex { 644 | const text = switch (downloadToString(allocator, index_url)) { 645 | .ok => |text| text, 646 | .err => |err| { 647 | std.log.err("could not download '{s}': {s}", .{ index_url, err }); 648 | return error.AlreadyReported; 649 | }, 650 | }; 651 | errdefer allocator.free(text); 652 | var json = std.json.parseFromSlice(std.json.Value, allocator, text, .{}) catch |e| { 653 | std.log.err( 654 | "failed to parse JSON content from index url '{s}' with {s}", 655 | .{ index_url, @errorName(e) }, 656 | ); 657 | return error.AlreadyReported; 658 | }; 659 | errdefer json.deinit(); 660 | return DownloadIndex{ .text = text, .json = json }; 661 | } 662 | 663 | fn loggyMakePath(dir_absolute: []const u8) !void { 664 | if (builtin.os.tag == .windows) { 665 | loginfo("mkdir \"{s}\"", .{dir_absolute}); 666 | } else { 667 | loginfo("mkdir -p '{s}'", .{dir_absolute}); 668 | } 669 | try std.fs.cwd().makePath(dir_absolute); 670 | } 671 | 672 | fn loggyDeleteTreeAbsolute(dir_absolute: []const u8) !void { 673 | if (builtin.os.tag == .windows) { 674 | loginfo("rd /s /q \"{s}\"", .{dir_absolute}); 675 | } else { 676 | loginfo("rm -rf '{s}'", .{dir_absolute}); 677 | } 678 | try fixdeletetree.deleteTreeAbsolute(dir_absolute); 679 | } 680 | 681 | pub fn loggyRenameAbsolute(old_path: []const u8, new_path: []const u8) !void { 682 | loginfo("mv '{s}' '{s}'", .{ old_path, new_path }); 683 | try std.fs.renameAbsolute(old_path, new_path); 684 | } 685 | 686 | pub fn loggySymlinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags: std.fs.Dir.SymLinkFlags) !void { 687 | loginfo("ln -s '{s}' '{s}'", .{ target_path, sym_link_path }); 688 | // NOTE: can't use symLinkAbsolute because it requires target_path to be absolute but we don't want that 689 | // not sure if it is a bug in the standard lib or not 690 | //try std.fs.symLinkAbsolute(target_path, sym_link_path, flags); 691 | _ = flags; 692 | try std.posix.symlink(target_path, sym_link_path); 693 | } 694 | 695 | /// returns: true if the symlink was updated, false if it was already set to the given `target_path` 696 | pub fn loggyUpdateSymlink(target_path: []const u8, sym_link_path: []const u8, flags: std.fs.Dir.SymLinkFlags) !bool { 697 | var current_target_path_buffer: [std.fs.max_path_bytes]u8 = undefined; 698 | if (std.fs.readLinkAbsolute(sym_link_path, ¤t_target_path_buffer)) |current_target_path| { 699 | if (std.mem.eql(u8, target_path, current_target_path)) { 700 | loginfo("symlink '{s}' already points to '{s}'", .{ sym_link_path, target_path }); 701 | return false; // already up-to-date 702 | } 703 | try std.posix.unlink(sym_link_path); 704 | } else |e| switch (e) { 705 | error.FileNotFound => {}, 706 | error.NotLink => { 707 | std.debug.print( 708 | "unable to update/overwrite the 'zig' PATH symlink, the file '{s}' already exists and is not a symlink\n", 709 | .{sym_link_path}, 710 | ); 711 | std.process.exit(1); 712 | }, 713 | else => return e, 714 | } 715 | try loggySymlinkAbsolute(target_path, sym_link_path, flags); 716 | return true; // updated 717 | } 718 | 719 | // TODO: this should be in std lib somewhere 720 | fn existsAbsolute(absolutePath: []const u8) !bool { 721 | std.fs.cwd().access(absolutePath, .{}) catch |e| switch (e) { 722 | error.FileNotFound => return false, 723 | error.PermissionDenied => return e, 724 | error.InputOutput => return e, 725 | error.SystemResources => return e, 726 | error.SymLinkLoop => return e, 727 | error.FileBusy => return e, 728 | error.Unexpected => unreachable, 729 | error.InvalidUtf8 => return e, 730 | error.InvalidWtf8 => return e, 731 | error.ReadOnlyFileSystem => unreachable, 732 | error.NameTooLong => unreachable, 733 | error.BadPathName => unreachable, 734 | }; 735 | return true; 736 | } 737 | 738 | fn listCompilers(allocator: Allocator) !void { 739 | const install_dir_string = try getInstallDir(allocator, .{ .create = false }); 740 | defer allocator.free(install_dir_string); 741 | 742 | var install_dir = std.fs.openDirAbsolute(install_dir_string, .{ .iterate = true }) catch |e| switch (e) { 743 | error.FileNotFound => return, 744 | else => return e, 745 | }; 746 | defer install_dir.close(); 747 | 748 | const stdout = std.io.getStdOut().writer(); 749 | { 750 | var it = install_dir.iterate(); 751 | while (try it.next()) |entry| { 752 | if (entry.kind != .directory) 753 | continue; 754 | if (std.mem.endsWith(u8, entry.name, ".installing")) 755 | continue; 756 | try stdout.print("{s}\n", .{entry.name}); 757 | } 758 | } 759 | } 760 | 761 | fn keepCompiler(allocator: Allocator, compiler_version: []const u8) !void { 762 | const install_dir_string = try getInstallDir(allocator, .{ .create = true }); 763 | defer allocator.free(install_dir_string); 764 | 765 | var install_dir = try std.fs.openDirAbsolute(install_dir_string, .{ .iterate = true }); 766 | defer install_dir.close(); 767 | 768 | var compiler_dir = install_dir.openDir(compiler_version, .{}) catch |e| switch (e) { 769 | error.FileNotFound => { 770 | std.log.err("compiler not found: {s}", .{compiler_version}); 771 | return error.AlreadyReported; 772 | }, 773 | else => return e, 774 | }; 775 | var keep_fd = try compiler_dir.createFile("keep", .{}); 776 | keep_fd.close(); 777 | loginfo("created '{s}{c}{s}{c}{s}'", .{ install_dir_string, std.fs.path.sep, compiler_version, std.fs.path.sep, "keep" }); 778 | } 779 | 780 | fn cleanCompilers(allocator: Allocator, compiler_name_opt: ?[]const u8) !void { 781 | const install_dir_string = try getInstallDir(allocator, .{ .create = true }); 782 | defer allocator.free(install_dir_string); 783 | // getting the current compiler 784 | const default_comp_opt = try getDefaultCompiler(allocator); 785 | defer if (default_comp_opt) |default_compiler| allocator.free(default_compiler); 786 | 787 | var install_dir = std.fs.openDirAbsolute(install_dir_string, .{ .iterate = true }) catch |e| switch (e) { 788 | error.FileNotFound => return, 789 | else => return e, 790 | }; 791 | defer install_dir.close(); 792 | const master_points_to_opt = try getMasterDir(allocator, &install_dir); 793 | defer if (master_points_to_opt) |master_points_to| allocator.free(master_points_to); 794 | if (compiler_name_opt) |compiler_name| { 795 | if (getKeepReason(master_points_to_opt, default_comp_opt, compiler_name)) |reason| { 796 | std.log.err("cannot clean '{s}' ({s})", .{ compiler_name, reason }); 797 | return error.AlreadyReported; 798 | } 799 | loginfo("deleting '{s}{c}{s}'", .{ install_dir_string, std.fs.path.sep, compiler_name }); 800 | try fixdeletetree.deleteTree(install_dir, compiler_name); 801 | } else { 802 | var it = install_dir.iterate(); 803 | while (try it.next()) |entry| { 804 | if (entry.kind != .directory) 805 | continue; 806 | if (getKeepReason(master_points_to_opt, default_comp_opt, entry.name)) |reason| { 807 | loginfo("keeping '{s}' ({s})", .{ entry.name, reason }); 808 | continue; 809 | } 810 | 811 | { 812 | var compiler_dir = try install_dir.openDir(entry.name, .{}); 813 | defer compiler_dir.close(); 814 | if (compiler_dir.access("keep", .{})) |_| { 815 | loginfo("keeping '{s}' (has keep file)", .{entry.name}); 816 | continue; 817 | } else |e| switch (e) { 818 | error.FileNotFound => {}, 819 | else => return e, 820 | } 821 | } 822 | loginfo("deleting '{s}{c}{s}'", .{ install_dir_string, std.fs.path.sep, entry.name }); 823 | try fixdeletetree.deleteTree(install_dir, entry.name); 824 | } 825 | } 826 | } 827 | fn readDefaultCompiler(allocator: Allocator, buffer: *[std.fs.max_path_bytes + 1]u8) !?[]const u8 { 828 | const path_link = try makeZigPathLinkString(allocator); 829 | defer allocator.free(path_link); 830 | 831 | if (builtin.os.tag == .windows) { 832 | var file = std.fs.openFileAbsolute(path_link, .{}) catch |e| switch (e) { 833 | error.FileNotFound => return null, 834 | else => return e, 835 | }; 836 | defer file.close(); 837 | try file.seekTo(win32exelink.exe_offset); 838 | const len = try file.readAll(buffer); 839 | if (len != buffer.len) { 840 | std.log.err("path link file '{s}' is too small", .{path_link}); 841 | return error.AlreadyReported; 842 | } 843 | const target_exe = std.mem.sliceTo(buffer, 0); 844 | return try allocator.dupe(u8, targetPathToVersion(target_exe)); 845 | } 846 | 847 | const target_path = std.fs.readLinkAbsolute(path_link, buffer[0..std.fs.max_path_bytes]) catch |e| switch (e) { 848 | error.FileNotFound => return null, 849 | else => return e, 850 | }; 851 | defer allocator.free(target_path); 852 | return try allocator.dupe(u8, targetPathToVersion(target_path)); 853 | } 854 | fn targetPathToVersion(target_path: []const u8) []const u8 { 855 | return std.fs.path.basename(std.fs.path.dirname(std.fs.path.dirname(target_path).?).?); 856 | } 857 | 858 | fn readMasterDir(buffer: *[std.fs.max_path_bytes]u8, install_dir: *std.fs.Dir) !?[]const u8 { 859 | if (builtin.os.tag == .windows) { 860 | var file = install_dir.openFile("master", .{}) catch |e| switch (e) { 861 | error.FileNotFound => return null, 862 | else => return e, 863 | }; 864 | defer file.close(); 865 | return buffer[0..try file.readAll(buffer)]; 866 | } 867 | return install_dir.readLink("master", buffer) catch |e| switch (e) { 868 | error.FileNotFound => return null, 869 | else => return e, 870 | }; 871 | } 872 | 873 | fn getDefaultCompiler(allocator: Allocator) !?[]const u8 { 874 | var buffer: [std.fs.max_path_bytes + 1]u8 = undefined; 875 | const slice_path = (try readDefaultCompiler(allocator, &buffer)) orelse return null; 876 | const path_to_return = try allocator.alloc(u8, slice_path.len); 877 | @memcpy(path_to_return, slice_path); 878 | return path_to_return; 879 | } 880 | 881 | fn getMasterDir(allocator: Allocator, install_dir: *std.fs.Dir) !?[]const u8 { 882 | var buffer: [std.fs.max_path_bytes]u8 = undefined; 883 | const slice_path = (try readMasterDir(&buffer, install_dir)) orelse return null; 884 | const path_to_return = try allocator.alloc(u8, slice_path.len); 885 | @memcpy(path_to_return, slice_path); 886 | return path_to_return; 887 | } 888 | 889 | fn printDefaultCompiler(allocator: Allocator) !void { 890 | const default_compiler_opt = try getDefaultCompiler(allocator); 891 | defer if (default_compiler_opt) |default_compiler| allocator.free(default_compiler); 892 | const stdout = std.io.getStdOut().writer(); 893 | if (default_compiler_opt) |default_compiler| { 894 | try stdout.print("{s}\n", .{default_compiler}); 895 | } else { 896 | try stdout.writeAll("\n"); 897 | } 898 | } 899 | 900 | const ExistVerify = enum { existence_verified, verify_existence }; 901 | 902 | fn setDefaultCompiler(allocator: Allocator, compiler_dir: []const u8, exist_verify: ExistVerify) !void { 903 | switch (exist_verify) { 904 | .existence_verified => {}, 905 | .verify_existence => { 906 | var dir = std.fs.openDirAbsolute(compiler_dir, .{}) catch |err| switch (err) { 907 | error.FileNotFound => { 908 | std.log.err("compiler '{s}' is not installed", .{std.fs.path.basename(compiler_dir)}); 909 | return error.AlreadyReported; 910 | }, 911 | else => |e| return e, 912 | }; 913 | dir.close(); 914 | }, 915 | } 916 | 917 | const path_link = try makeZigPathLinkString(allocator); 918 | defer allocator.free(path_link); 919 | 920 | const link_target = try std.fs.path.join( 921 | allocator, 922 | &[_][]const u8{ compiler_dir, "files", comptime "zig" ++ builtin.target.exeFileExt() }, 923 | ); 924 | defer allocator.free(link_target); 925 | if (builtin.os.tag == .windows) { 926 | try createExeLink(link_target, path_link); 927 | } else { 928 | _ = try loggyUpdateSymlink(link_target, path_link, .{}); 929 | } 930 | 931 | try verifyPathLink(allocator, path_link); 932 | } 933 | 934 | /// Verify that path_link will work. It verifies that `path_link` is 935 | /// in PATH and there is no zig executable in an earlier directory in PATH. 936 | fn verifyPathLink(allocator: Allocator, path_link: []const u8) !void { 937 | const path_link_dir = std.fs.path.dirname(path_link) orelse { 938 | std.log.err("invalid '--path-link' '{s}', it must be a file (not the root directory)", .{path_link}); 939 | return error.AlreadyReported; 940 | }; 941 | 942 | const path_link_dir_id = blk: { 943 | var dir = std.fs.openDirAbsolute(path_link_dir, .{}) catch |err| { 944 | std.log.err("unable to open the path-link directory '{s}': {s}", .{ path_link_dir, @errorName(err) }); 945 | return error.AlreadyReported; 946 | }; 947 | defer dir.close(); 948 | break :blk try FileId.initFromDir(dir, path_link); 949 | }; 950 | 951 | if (builtin.os.tag == .windows) { 952 | const path_env = std.process.getEnvVarOwned(allocator, "PATH") catch |err| switch (err) { 953 | error.EnvironmentVariableNotFound => return, 954 | else => |e| return e, 955 | }; 956 | defer allocator.free(path_env); 957 | 958 | var free_pathext: ?[]const u8 = null; 959 | defer if (free_pathext) |p| allocator.free(p); 960 | 961 | const pathext_env = blk: { 962 | if (std.process.getEnvVarOwned(allocator, "PATHEXT")) |env| { 963 | free_pathext = env; 964 | break :blk env; 965 | } else |err| switch (err) { 966 | error.EnvironmentVariableNotFound => break :blk "", 967 | else => |e| return e, 968 | } 969 | break :blk ""; 970 | }; 971 | 972 | var path_it = std.mem.tokenizeScalar(u8, path_env, ';'); 973 | while (path_it.next()) |path| { 974 | switch (try compareDir(path_link_dir_id, path)) { 975 | .missing => continue, 976 | // can't be the same directory because we were able to open and get 977 | // the file id for path_link_dir_id 978 | .access_denied => {}, 979 | .match => return, 980 | .mismatch => {}, 981 | } 982 | { 983 | const exe = try std.fs.path.join(allocator, &.{ path, "zig" }); 984 | defer allocator.free(exe); 985 | try enforceNoZig(path_link, exe); 986 | } 987 | 988 | var ext_it = std.mem.tokenizeScalar(u8, pathext_env, ';'); 989 | while (ext_it.next()) |ext| { 990 | if (ext.len == 0) continue; 991 | const basename = try std.mem.concat(allocator, u8, &.{ "zig", ext }); 992 | defer allocator.free(basename); 993 | 994 | const exe = try std.fs.path.join(allocator, &.{ path, basename }); 995 | defer allocator.free(exe); 996 | 997 | try enforceNoZig(path_link, exe); 998 | } 999 | } 1000 | } else { 1001 | var path_it = std.mem.tokenizeScalar(u8, std.posix.getenv("PATH") orelse "", ':'); 1002 | while (path_it.next()) |path| { 1003 | switch (try compareDir(path_link_dir_id, path)) { 1004 | .missing => continue, 1005 | // can't be the same directory because we were able to open and get 1006 | // the file id for path_link_dir_id 1007 | .access_denied => {}, 1008 | .match => return, 1009 | .mismatch => {}, 1010 | } 1011 | const exe = try std.fs.path.join(allocator, &.{ path, "zig" }); 1012 | defer allocator.free(exe); 1013 | try enforceNoZig(path_link, exe); 1014 | } 1015 | } 1016 | 1017 | std.log.err("the path link '{s}' is not in PATH", .{path_link}); 1018 | return error.AlreadyReported; 1019 | } 1020 | 1021 | fn compareDir(dir_id: FileId, other_dir: []const u8) !enum { missing, access_denied, match, mismatch } { 1022 | var dir = std.fs.cwd().openDir(other_dir, .{}) catch |err| switch (err) { 1023 | error.FileNotFound, error.NotDir, error.BadPathName => return .missing, 1024 | error.AccessDenied => return .access_denied, 1025 | else => |e| return e, 1026 | }; 1027 | defer dir.close(); 1028 | return if (dir_id.eql(try FileId.initFromDir(dir, other_dir))) .match else .mismatch; 1029 | } 1030 | 1031 | fn enforceNoZig(path_link: []const u8, exe: []const u8) !void { 1032 | var file = std.fs.cwd().openFile(exe, .{}) catch |err| switch (err) { 1033 | error.FileNotFound, error.IsDir => return, 1034 | error.AccessDenied => return, // if there is a Zig it must not be accessible 1035 | else => |e| return e, 1036 | }; 1037 | defer file.close(); 1038 | 1039 | // todo: on posix systems ignore the file if it is not executable 1040 | std.log.err("zig compiler '{s}' is higher priority in PATH than the path-link '{s}'", .{ exe, path_link }); 1041 | } 1042 | 1043 | const FileId = struct { 1044 | dev: if (builtin.os.tag == .windows) u32 else blk: { 1045 | const st: std.posix.Stat = undefined; 1046 | break :blk @TypeOf(st.dev); 1047 | }, 1048 | ino: if (builtin.os.tag == .windows) u64 else blk: { 1049 | const st: std.posix.Stat = undefined; 1050 | break :blk @TypeOf(st.ino); 1051 | }, 1052 | 1053 | pub fn initFromFile(file: std.fs.File, filename_for_error: []const u8) !FileId { 1054 | if (builtin.os.tag == .windows) { 1055 | var info: win32.BY_HANDLE_FILE_INFORMATION = undefined; 1056 | if (0 == win32.GetFileInformationByHandle(file.handle, &info)) { 1057 | std.log.err( 1058 | "GetFileInformationByHandle on '{s}' failed, error={}", 1059 | .{ filename_for_error, @intFromEnum(std.os.windows.kernel32.GetLastError()) }, 1060 | ); 1061 | return error.AlreadyReported; 1062 | } 1063 | return FileId{ 1064 | .dev = info.dwVolumeSerialNumber, 1065 | .ino = (@as(u64, @intCast(info.nFileIndexHigh)) << 32) | @as(u64, @intCast(info.nFileIndexLow)), 1066 | }; 1067 | } 1068 | const st = try std.posix.fstat(file.handle); 1069 | return FileId{ 1070 | .dev = st.dev, 1071 | .ino = st.ino, 1072 | }; 1073 | } 1074 | 1075 | pub fn initFromDir(dir: std.fs.Dir, name_for_error: []const u8) !FileId { 1076 | if (builtin.os.tag == .windows) { 1077 | return initFromFile(std.fs.File{ .handle = dir.fd }, name_for_error); 1078 | } 1079 | return initFromFile(std.fs.File{ .handle = dir.fd }, name_for_error); 1080 | } 1081 | 1082 | pub fn eql(self: FileId, other: FileId) bool { 1083 | return self.dev == other.dev and self.ino == other.ino; 1084 | } 1085 | }; 1086 | 1087 | const win32 = struct { 1088 | pub const BOOL = i32; 1089 | pub const FILETIME = extern struct { 1090 | dwLowDateTime: u32, 1091 | dwHighDateTime: u32, 1092 | }; 1093 | pub const BY_HANDLE_FILE_INFORMATION = extern struct { 1094 | dwFileAttributes: u32, 1095 | ftCreationTime: FILETIME, 1096 | ftLastAccessTime: FILETIME, 1097 | ftLastWriteTime: FILETIME, 1098 | dwVolumeSerialNumber: u32, 1099 | nFileSizeHigh: u32, 1100 | nFileSizeLow: u32, 1101 | nNumberOfLinks: u32, 1102 | nFileIndexHigh: u32, 1103 | nFileIndexLow: u32, 1104 | }; 1105 | pub extern "kernel32" fn GetFileInformationByHandle( 1106 | hFile: ?@import("std").os.windows.HANDLE, 1107 | lpFileInformation: ?*BY_HANDLE_FILE_INFORMATION, 1108 | ) callconv(@import("std").os.windows.WINAPI) BOOL; 1109 | }; 1110 | 1111 | const win32exelink = struct { 1112 | const content = @embedFile("win32exelink"); 1113 | const exe_offset: usize = if (builtin.os.tag != .windows) 0 else blk: { 1114 | @setEvalBranchQuota(content.len * 2); 1115 | const marker = "!!!THIS MARKS THE zig_exe_string MEMORY!!#"; 1116 | const offset = std.mem.indexOf(u8, content, marker) orelse { 1117 | @compileError("win32exelink is missing the marker: " ++ marker); 1118 | }; 1119 | if (std.mem.indexOf(u8, content[offset + 1 ..], marker) != null) { 1120 | @compileError("win32exelink contains multiple markers (not implemented)"); 1121 | } 1122 | break :blk offset + marker.len; 1123 | }; 1124 | }; 1125 | fn createExeLink(link_target: []const u8, path_link: []const u8) !void { 1126 | if (path_link.len > std.fs.max_path_bytes) { 1127 | std.debug.print("Error: path_link (size {}) is too large (max {})\n", .{ path_link.len, std.fs.max_path_bytes }); 1128 | return error.AlreadyReported; 1129 | } 1130 | const file = std.fs.cwd().createFile(path_link, .{}) catch |err| switch (err) { 1131 | error.IsDir => { 1132 | std.debug.print( 1133 | "unable to create the exe link, the path '{s}' is a directory\n", 1134 | .{path_link}, 1135 | ); 1136 | std.process.exit(1); 1137 | }, 1138 | else => |e| return e, 1139 | }; 1140 | defer file.close(); 1141 | try file.writer().writeAll(win32exelink.content[0..win32exelink.exe_offset]); 1142 | try file.writer().writeAll(link_target); 1143 | try file.writer().writeAll(win32exelink.content[win32exelink.exe_offset + link_target.len ..]); 1144 | } 1145 | 1146 | const Release = struct { 1147 | major: usize, 1148 | minor: usize, 1149 | patch: usize, 1150 | pub fn order(a: Release, b: Release) std.math.Order { 1151 | if (a.major != b.major) return std.math.order(a.major, b.major); 1152 | if (a.minor != b.minor) return std.math.order(a.minor, b.minor); 1153 | return std.math.order(a.patch, b.patch); 1154 | } 1155 | }; 1156 | 1157 | // The Zig release where the OS-ARCH in the url was swapped to ARCH-OS 1158 | const arch_os_swap_release: Release = .{ .major = 0, .minor = 14, .patch = 1 }; 1159 | 1160 | const SemanticVersion = struct { 1161 | const max_pre = 50; 1162 | const max_build = 50; 1163 | const max_string = 50 + max_pre + max_build; 1164 | 1165 | major: usize, 1166 | minor: usize, 1167 | patch: usize, 1168 | pre: ?std.BoundedArray(u8, max_pre), 1169 | build: ?std.BoundedArray(u8, max_build), 1170 | 1171 | pub fn array(self: *const SemanticVersion) std.BoundedArray(u8, max_string) { 1172 | var result: std.BoundedArray(u8, max_string) = undefined; 1173 | const roundtrip = std.fmt.bufPrint(&result.buffer, "{}", .{self}) catch unreachable; 1174 | result.len = roundtrip.len; 1175 | return result; 1176 | } 1177 | 1178 | pub fn parse(s: []const u8) ?SemanticVersion { 1179 | const parsed = std.SemanticVersion.parse(s) catch |e| switch (e) { 1180 | error.Overflow, error.InvalidVersion => return null, 1181 | }; 1182 | std.debug.assert(s.len <= max_string); 1183 | 1184 | var result: SemanticVersion = .{ 1185 | .major = parsed.major, 1186 | .minor = parsed.minor, 1187 | .patch = parsed.patch, 1188 | .pre = if (parsed.pre) |pre| std.BoundedArray(u8, max_pre).init(pre.len) catch |e| switch (e) { 1189 | error.Overflow => std.debug.panic("semantic version pre '{s}' is too long (max is {})", .{ pre, max_pre }), 1190 | } else null, 1191 | .build = if (parsed.build) |build| std.BoundedArray(u8, max_build).init(build.len) catch |e| switch (e) { 1192 | error.Overflow => std.debug.panic("semantic version build '{s}' is too long (max is {})", .{ build, max_build }), 1193 | } else null, 1194 | }; 1195 | if (parsed.pre) |pre| @memcpy(result.pre.?.slice(), pre); 1196 | if (parsed.build) |build| @memcpy(result.build.?.slice(), build); 1197 | 1198 | { 1199 | // sanity check, ensure format gives us the same string back we just parsed 1200 | const roundtrip = result.array(); 1201 | if (!std.mem.eql(u8, roundtrip.slice(), s)) std.debug.panic( 1202 | "codebug parse/format version mismatch:\nparsed: '{s}'\nformat: '{s}'\n", 1203 | .{ s, roundtrip.slice() }, 1204 | ); 1205 | } 1206 | 1207 | return result; 1208 | } 1209 | pub fn ref(self: *const SemanticVersion) std.SemanticVersion { 1210 | return .{ 1211 | .major = self.major, 1212 | .minor = self.minor, 1213 | .patch = self.patch, 1214 | .pre = if (self.pre) |*pre| pre.slice() else null, 1215 | .build = if (self.build) |*build| build.slice() else null, 1216 | }; 1217 | } 1218 | pub fn format( 1219 | self: SemanticVersion, 1220 | comptime fmt: []const u8, 1221 | options: std.fmt.FormatOptions, 1222 | writer: anytype, 1223 | ) !void { 1224 | try self.ref().format(fmt, options, writer); 1225 | } 1226 | }; 1227 | 1228 | fn getDefaultUrl(allocator: Allocator, compiler_version: []const u8) ![]const u8 { 1229 | const sv = SemanticVersion.parse(compiler_version) orelse errExit( 1230 | "invalid zig version '{s}', unable to create a download URL for it", 1231 | .{compiler_version}, 1232 | ); 1233 | if (sv.pre != null or sv.build != null) return try std.fmt.allocPrint( 1234 | allocator, 1235 | "https://ziglang.org/builds/zig-" ++ arch_os ++ "-{0s}." ++ archive_ext, 1236 | .{compiler_version}, 1237 | ); 1238 | const release: Release = .{ .major = sv.major, .minor = sv.minor, .patch = sv.patch }; 1239 | return try std.fmt.allocPrint( 1240 | allocator, 1241 | "https://ziglang.org/download/{s}/zig-{1s}-{0s}." ++ archive_ext, 1242 | .{ 1243 | compiler_version, 1244 | switch (release.order(arch_os_swap_release)) { 1245 | .lt => os_arch, 1246 | .gt, .eq => arch_os, 1247 | }, 1248 | }, 1249 | ); 1250 | } 1251 | 1252 | fn installCompiler(allocator: Allocator, compiler_dir: []const u8, url: []const u8) !void { 1253 | if (try existsAbsolute(compiler_dir)) { 1254 | loginfo("compiler '{s}' already installed", .{compiler_dir}); 1255 | return; 1256 | } 1257 | 1258 | const installing_dir = try std.mem.concat(allocator, u8, &[_][]const u8{ compiler_dir, ".installing" }); 1259 | defer allocator.free(installing_dir); 1260 | try loggyDeleteTreeAbsolute(installing_dir); 1261 | try loggyMakePath(installing_dir); 1262 | 1263 | const archive_basename = std.fs.path.basename(url); 1264 | var archive_root_dir: []const u8 = undefined; 1265 | 1266 | // download and extract archive 1267 | { 1268 | const archive_absolute = try std.fs.path.join(allocator, &[_][]const u8{ installing_dir, archive_basename }); 1269 | defer allocator.free(archive_absolute); 1270 | loginfo("downloading '{s}' to '{s}'", .{ url, archive_absolute }); 1271 | 1272 | switch (blk: { 1273 | const file = try std.fs.createFileAbsolute(archive_absolute, .{}); 1274 | // note: important to close the file before we handle errors below 1275 | // since it will delete the parent directory of this file 1276 | defer file.close(); 1277 | break :blk download(allocator, url, file.writer()); 1278 | }) { 1279 | .ok => {}, 1280 | .err => |err| { 1281 | std.log.err("could not download '{s}': {s}", .{ url, err }); 1282 | // this removes the installing dir if the http request fails so we dont have random directories 1283 | try loggyDeleteTreeAbsolute(installing_dir); 1284 | return error.AlreadyReported; 1285 | }, 1286 | } 1287 | 1288 | if (std.mem.endsWith(u8, archive_basename, ".tar.xz")) { 1289 | archive_root_dir = archive_basename[0 .. archive_basename.len - ".tar.xz".len]; 1290 | _ = try run(allocator, &[_][]const u8{ "tar", "xf", archive_absolute, "-C", installing_dir }); 1291 | } else { 1292 | var recognized = false; 1293 | if (builtin.os.tag == .windows) { 1294 | if (std.mem.endsWith(u8, archive_basename, ".zip")) { 1295 | recognized = true; 1296 | archive_root_dir = archive_basename[0 .. archive_basename.len - ".zip".len]; 1297 | 1298 | var installing_dir_opened = try std.fs.openDirAbsolute(installing_dir, .{}); 1299 | defer installing_dir_opened.close(); 1300 | loginfo("extracting archive to \"{s}\"", .{installing_dir}); 1301 | var timer = try std.time.Timer.start(); 1302 | var archive_file = try std.fs.openFileAbsolute(archive_absolute, .{}); 1303 | defer archive_file.close(); 1304 | try std.zip.extract(installing_dir_opened, archive_file.seekableStream(), .{}); 1305 | const time = timer.read(); 1306 | loginfo("extracted archive in {d:.2} s", .{@as(f32, @floatFromInt(time)) / @as(f32, @floatFromInt(std.time.ns_per_s))}); 1307 | } 1308 | } 1309 | 1310 | if (!recognized) { 1311 | std.log.err("unknown archive extension '{s}'", .{archive_basename}); 1312 | return error.UnknownArchiveExtension; 1313 | } 1314 | } 1315 | try loggyDeleteTreeAbsolute(archive_absolute); 1316 | } 1317 | 1318 | { 1319 | const extracted_dir = try std.fs.path.join(allocator, &[_][]const u8{ installing_dir, archive_root_dir }); 1320 | defer allocator.free(extracted_dir); 1321 | const normalized_dir = try std.fs.path.join(allocator, &[_][]const u8{ installing_dir, "files" }); 1322 | defer allocator.free(normalized_dir); 1323 | try loggyRenameAbsolute(extracted_dir, normalized_dir); 1324 | } 1325 | 1326 | // TODO: write date information (so users can sort compilers by date) 1327 | 1328 | // finish installation by renaming the install dir 1329 | try loggyRenameAbsolute(installing_dir, compiler_dir); 1330 | } 1331 | 1332 | pub fn run(allocator: Allocator, argv: []const []const u8) !std.process.Child.Term { 1333 | try logRun(allocator, argv); 1334 | var proc = std.process.Child.init(argv, allocator); 1335 | return proc.spawnAndWait(); 1336 | } 1337 | 1338 | fn logRun(allocator: Allocator, argv: []const []const u8) !void { 1339 | var buffer = try allocator.alloc(u8, getCommandStringLength(argv)); 1340 | defer allocator.free(buffer); 1341 | 1342 | var prefix = false; 1343 | var offset: usize = 0; 1344 | for (argv) |arg| { 1345 | if (prefix) { 1346 | buffer[offset] = ' '; 1347 | offset += 1; 1348 | } else { 1349 | prefix = true; 1350 | } 1351 | @memcpy(buffer[offset .. offset + arg.len], arg); 1352 | offset += arg.len; 1353 | } 1354 | std.debug.assert(offset == buffer.len); 1355 | loginfo("[RUN] {s}", .{buffer}); 1356 | } 1357 | 1358 | fn errExit(comptime fmt: []const u8, args: anytype) noreturn { 1359 | std.log.err(fmt, args); 1360 | std.process.exit(0xff); 1361 | } 1362 | 1363 | pub fn getCommandStringLength(argv: []const []const u8) usize { 1364 | var len: usize = 0; 1365 | var prefix_length: u8 = 0; 1366 | for (argv) |arg| { 1367 | len += prefix_length + arg.len; 1368 | prefix_length = 1; 1369 | } 1370 | return len; 1371 | } 1372 | 1373 | pub fn getKeepReason(master_points_to_opt: ?[]const u8, default_compiler_opt: ?[]const u8, name: []const u8) ?[]const u8 { 1374 | if (default_compiler_opt) |default_comp| { 1375 | if (mem.eql(u8, default_comp, name)) { 1376 | return "is default compiler"; 1377 | } 1378 | } 1379 | if (master_points_to_opt) |master_points_to| { 1380 | if (mem.eql(u8, master_points_to, name)) { 1381 | return "it is master"; 1382 | } 1383 | } 1384 | return null; 1385 | } 1386 | -------------------------------------------------------------------------------- /zip.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | 4 | fn oom(e: error{OutOfMemory}) noreturn { 5 | @panic(@errorName(e)); 6 | } 7 | fn fatal(comptime fmt: []const u8, args: anytype) noreturn { 8 | std.log.err(fmt, args); 9 | std.process.exit(0xff); 10 | } 11 | 12 | fn usage() noreturn { 13 | std.io.getStdErr().writer().writeAll( 14 | "Usage: zip [-options] ZIP_FILE FILES/DIRS..\n", 15 | ) catch |e| @panic(@errorName(e)); 16 | std.process.exit(1); 17 | } 18 | 19 | var windows_args_arena = if (builtin.os.tag == .windows) 20 | std.heap.ArenaAllocator.init(std.heap.page_allocator) 21 | else 22 | struct {}{}; 23 | pub fn cmdlineArgs() [][*:0]u8 { 24 | if (builtin.os.tag == .windows) { 25 | const slices = std.process.argsAlloc(windows_args_arena.allocator()) catch |err| switch (err) { 26 | error.OutOfMemory => oom(error.OutOfMemory), 27 | //error.InvalidCmdLine => @panic("InvalidCmdLine"), 28 | error.Overflow => @panic("Overflow while parsing command line"), 29 | }; 30 | const args = windows_args_arena.allocator().alloc([*:0]u8, slices.len - 1) catch |e| oom(e); 31 | for (slices[1..], 0..) |slice, i| { 32 | args[i] = slice.ptr; 33 | } 34 | return args; 35 | } 36 | return std.os.argv.ptr[1..std.os.argv.len]; 37 | } 38 | 39 | pub fn main() !void { 40 | var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); 41 | defer arena_instance.deinit(); 42 | const arena = arena_instance.allocator(); 43 | 44 | const cmd_args = blk: { 45 | const cmd_args = cmdlineArgs(); 46 | var arg_index: usize = 0; 47 | var non_option_len: usize = 0; 48 | while (arg_index < cmd_args.len) : (arg_index += 1) { 49 | const arg = std.mem.span(cmd_args[arg_index]); 50 | if (!std.mem.startsWith(u8, arg, "-")) { 51 | cmd_args[non_option_len] = arg; 52 | non_option_len += 1; 53 | } else { 54 | fatal("unknown cmdline option '{s}'", .{arg}); 55 | } 56 | } 57 | break :blk cmd_args[0..non_option_len]; 58 | }; 59 | 60 | if (cmd_args.len < 2) usage(); 61 | const zip_file_arg = std.mem.span(cmd_args[0]); 62 | const paths_to_include = cmd_args[1..]; 63 | 64 | // expand cmdline arguments to a list of files 65 | var file_entries: std.ArrayListUnmanaged(FileEntry) = .{}; 66 | for (paths_to_include) |path_ptr| { 67 | const path = std.mem.span(path_ptr); 68 | const stat = std.fs.cwd().statFile(path) catch |err| switch (err) { 69 | error.FileNotFound => fatal("path '{s}' is not found", .{path}), 70 | else => |e| return e, 71 | }; 72 | switch (stat.kind) { 73 | .directory => { 74 | @panic("todo: directories"); 75 | }, 76 | .file => { 77 | if (isBadFilename(path)) 78 | fatal("filename '{s}' is invalid for zip files", .{path}); 79 | try file_entries.append(arena, .{ 80 | .path = path, 81 | .size = stat.size, 82 | }); 83 | }, 84 | .sym_link => fatal("todo: symlinks", .{}), 85 | .block_device, 86 | .character_device, 87 | .named_pipe, 88 | .unix_domain_socket, 89 | .whiteout, 90 | .door, 91 | .event_port, 92 | .unknown, 93 | => fatal("file '{s}' is an unsupported type {s}", .{ path, @tagName(stat.kind) }), 94 | } 95 | } 96 | 97 | const store = try arena.alloc(FileStore, file_entries.items.len); 98 | // no need to free 99 | 100 | { 101 | const zip_file = std.fs.cwd().createFile(zip_file_arg, .{}) catch |err| 102 | fatal("create file '{s}' failed: {s}", .{ zip_file_arg, @errorName(err) }); 103 | defer zip_file.close(); 104 | try writeZip(zip_file, file_entries.items, store); 105 | } 106 | 107 | // go fix up the local file headers 108 | { 109 | const zip_file = std.fs.cwd().openFile(zip_file_arg, .{ .mode = .read_write }) catch |err| 110 | fatal("open file '{s}' failed: {s}", .{ zip_file_arg, @errorName(err) }); 111 | defer zip_file.close(); 112 | for (file_entries.items, 0..) |file, i| { 113 | try zip_file.seekTo(store[i].file_offset); 114 | const hdr: std.zip.LocalFileHeader = .{ 115 | .signature = std.zip.local_file_header_sig, 116 | .version_needed_to_extract = 10, 117 | .flags = .{ .encrypted = false, ._ = 0 }, 118 | .compression_method = store[i].compression, 119 | .last_modification_time = 0, 120 | .last_modification_date = 0, 121 | .crc32 = store[i].crc32, 122 | .compressed_size = store[i].compressed_size, 123 | .uncompressed_size = @intCast(file.size), 124 | .filename_len = @intCast(file.path.len), 125 | .extra_len = 0, 126 | }; 127 | try writeStructEndian(zip_file.writer(), hdr, .little); 128 | } 129 | } 130 | } 131 | 132 | const FileEntry = struct { 133 | path: []const u8, 134 | size: u64, 135 | }; 136 | 137 | fn writeZip( 138 | out_zip: std.fs.File, 139 | file_entries: []const FileEntry, 140 | store: []FileStore, 141 | ) !void { 142 | var zipper = initZipper(out_zip.writer()); 143 | for (file_entries, 0..) |file_entry, i| { 144 | const file_offset = zipper.counting_writer.bytes_written; 145 | 146 | const compression: std.zip.CompressionMethod = .deflate; 147 | 148 | try zipper.writeFileHeader(file_entry.path, compression); 149 | 150 | var file = try std.fs.cwd().openFile(file_entry.path, .{}); 151 | defer file.close(); 152 | 153 | var crc32: u32 = undefined; 154 | 155 | var compressed_size = file_entry.size; 156 | switch (compression) { 157 | .store => { 158 | var hash = std.hash.Crc32.init(); 159 | var full_rw_buf: [std.mem.page_size]u8 = undefined; 160 | var remaining = file_entry.size; 161 | while (remaining > 0) { 162 | const buf = full_rw_buf[0..@min(remaining, full_rw_buf.len)]; 163 | const read_len = try file.reader().read(buf); 164 | std.debug.assert(read_len == buf.len); 165 | hash.update(buf); 166 | try zipper.counting_writer.writer().writeAll(buf); 167 | remaining -= buf.len; 168 | } 169 | crc32 = hash.final(); 170 | }, 171 | .deflate => { 172 | const start_offset = zipper.counting_writer.bytes_written; 173 | var br = std.io.bufferedReader(file.reader()); 174 | var cr = Crc32Reader(@TypeOf(br.reader())){ .underlying_reader = br.reader() }; 175 | 176 | try std.compress.flate.deflate.compress( 177 | .raw, 178 | cr.reader(), 179 | zipper.counting_writer.writer(), 180 | .{ .level = .best }, 181 | ); 182 | if (br.end != br.start) fatal("deflate compressor didn't read all data", .{}); 183 | compressed_size = zipper.counting_writer.bytes_written - start_offset; 184 | crc32 = cr.crc32.final(); 185 | }, 186 | else => @panic("codebug"), 187 | } 188 | store[i] = .{ 189 | .file_offset = file_offset, 190 | .compression = compression, 191 | .uncompressed_size = @intCast(file_entry.size), 192 | .crc32 = crc32, 193 | .compressed_size = @intCast(compressed_size), 194 | }; 195 | } 196 | for (file_entries, 0..) |file, i| { 197 | try zipper.writeCentralRecord(store[i], .{ 198 | .name = file.path, 199 | }); 200 | } 201 | try zipper.writeEndRecord(); 202 | } 203 | 204 | pub fn Crc32Reader(comptime ReaderType: type) type { 205 | return struct { 206 | underlying_reader: ReaderType, 207 | crc32: std.hash.Crc32 = std.hash.Crc32.init(), 208 | 209 | pub const Error = ReaderType.Error; 210 | pub const Reader = std.io.Reader(*Self, Error, read); 211 | 212 | const Self = @This(); 213 | 214 | pub fn read(self: *Self, dest: []u8) Error!usize { 215 | const len = try self.underlying_reader.read(dest); 216 | self.crc32.update(dest[0..len]); 217 | return len; 218 | } 219 | 220 | pub fn reader(self: *Self) Reader { 221 | return .{ .context = self }; 222 | } 223 | }; 224 | } 225 | 226 | fn isBadFilename(filename: []const u8) bool { 227 | if (std.mem.indexOfScalar(u8, filename, '\\')) |_| 228 | return true; 229 | 230 | if (filename.len == 0 or filename[0] == '/' or filename[0] == '\\') 231 | return true; 232 | 233 | var it = std.mem.splitAny(u8, filename, "/\\"); 234 | while (it.next()) |part| { 235 | if (std.mem.eql(u8, part, "..")) 236 | return true; 237 | } 238 | 239 | return false; 240 | } 241 | 242 | // Used to store any data from writing a file to the zip archive that's needed 243 | // when writing the corresponding central directory record. 244 | pub const FileStore = struct { 245 | file_offset: u64, 246 | compression: std.zip.CompressionMethod, 247 | uncompressed_size: u32, 248 | crc32: u32, 249 | compressed_size: u32, 250 | }; 251 | 252 | pub fn initZipper(writer: anytype) Zipper(@TypeOf(writer)) { 253 | return .{ .counting_writer = std.io.countingWriter(writer) }; 254 | } 255 | 256 | fn Zipper(comptime Writer: type) type { 257 | return struct { 258 | counting_writer: std.io.CountingWriter(Writer), 259 | central_count: u64 = 0, 260 | first_central_offset: ?u64 = null, 261 | last_central_limit: ?u64 = null, 262 | 263 | const Self = @This(); 264 | 265 | pub fn writeFileHeader( 266 | self: *Self, 267 | name: []const u8, 268 | compression: std.zip.CompressionMethod, 269 | ) !void { 270 | const writer = self.counting_writer.writer(); 271 | const hdr: std.zip.LocalFileHeader = .{ 272 | .signature = std.zip.local_file_header_sig, 273 | .version_needed_to_extract = 10, 274 | .flags = .{ .encrypted = false, ._ = 0 }, 275 | .compression_method = compression, 276 | .last_modification_time = 0, 277 | .last_modification_date = 0, 278 | .crc32 = 0, 279 | .compressed_size = 0, 280 | .uncompressed_size = 0, 281 | .filename_len = @intCast(name.len), 282 | .extra_len = 0, 283 | }; 284 | try writeStructEndian(writer, hdr, .little); 285 | try writer.writeAll(name); 286 | } 287 | 288 | pub fn writeCentralRecord( 289 | self: *Self, 290 | store: FileStore, 291 | opt: struct { 292 | name: []const u8, 293 | version_needed_to_extract: u16 = 10, 294 | }, 295 | ) !void { 296 | if (self.first_central_offset == null) { 297 | self.first_central_offset = self.counting_writer.bytes_written; 298 | } 299 | self.central_count += 1; 300 | 301 | const hdr: std.zip.CentralDirectoryFileHeader = .{ 302 | .signature = std.zip.central_file_header_sig, 303 | .version_made_by = 0, 304 | .version_needed_to_extract = opt.version_needed_to_extract, 305 | .flags = .{ .encrypted = false, ._ = 0 }, 306 | .compression_method = store.compression, 307 | .last_modification_time = 0, 308 | .last_modification_date = 0, 309 | .crc32 = store.crc32, 310 | .compressed_size = store.compressed_size, 311 | .uncompressed_size = @intCast(store.uncompressed_size), 312 | .filename_len = @intCast(opt.name.len), 313 | .extra_len = 0, 314 | .comment_len = 0, 315 | .disk_number = 0, 316 | .internal_file_attributes = 0, 317 | .external_file_attributes = 0, 318 | .local_file_header_offset = @intCast(store.file_offset), 319 | }; 320 | try writeStructEndian(self.counting_writer.writer(), hdr, .little); 321 | try self.counting_writer.writer().writeAll(opt.name); 322 | self.last_central_limit = self.counting_writer.bytes_written; 323 | } 324 | 325 | pub fn writeEndRecord(self: *Self) !void { 326 | const cd_offset = self.first_central_offset orelse 0; 327 | const cd_end = self.last_central_limit orelse 0; 328 | const hdr: std.zip.EndRecord = .{ 329 | .signature = std.zip.end_record_sig, 330 | .disk_number = 0, 331 | .central_directory_disk_number = 0, 332 | .record_count_disk = @intCast(self.central_count), 333 | .record_count_total = @intCast(self.central_count), 334 | .central_directory_size = @intCast(cd_end - cd_offset), 335 | .central_directory_offset = @intCast(cd_offset), 336 | .comment_len = 0, 337 | }; 338 | try writeStructEndian(self.counting_writer.writer(), hdr, .little); 339 | } 340 | }; 341 | } 342 | 343 | const native_endian = @import("builtin").target.cpu.arch.endian(); 344 | 345 | fn writeStructEndian(writer: anytype, value: anytype, endian: std.builtin.Endian) anyerror!void { 346 | // TODO: make sure this value is not a reference type 347 | if (native_endian == endian) { 348 | return writer.writeStruct(value); 349 | } else { 350 | var copy = value; 351 | byteSwapAllFields(@TypeOf(value), ©); 352 | return writer.writeStruct(copy); 353 | } 354 | } 355 | pub fn byteSwapAllFields(comptime S: type, ptr: *S) void { 356 | switch (@typeInfo(S)) { 357 | .@"struct" => { 358 | inline for (std.meta.fields(S)) |f| { 359 | switch (@typeInfo(f.type)) { 360 | .@"struct" => |struct_info| if (struct_info.backing_integer) |Int| { 361 | @field(ptr, f.name) = @bitCast(@byteSwap(@as(Int, @bitCast(@field(ptr, f.name))))); 362 | } else { 363 | byteSwapAllFields(f.type, &@field(ptr, f.name)); 364 | }, 365 | .array => byteSwapAllFields(f.type, &@field(ptr, f.name)), 366 | .@"enum" => { 367 | @field(ptr, f.name) = @enumFromInt(@byteSwap(@intFromEnum(@field(ptr, f.name)))); 368 | }, 369 | else => { 370 | @field(ptr, f.name) = @byteSwap(@field(ptr, f.name)); 371 | }, 372 | } 373 | } 374 | }, 375 | .array => { 376 | for (ptr) |*item| { 377 | switch (@typeInfo(@TypeOf(item.*))) { 378 | .@"struct", .array => byteSwapAllFields(@TypeOf(item.*), item), 379 | .@"enum" => { 380 | item.* = @enumFromInt(@byteSwap(@intFromEnum(item.*))); 381 | }, 382 | else => { 383 | item.* = @byteSwap(item.*); 384 | }, 385 | } 386 | } 387 | }, 388 | else => @compileError("byteSwapAllFields expects a struct or array as the first argument"), 389 | } 390 | } 391 | --------------------------------------------------------------------------------