├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── compile.zig └── fetch.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-out/ 2 | zig-cache/ 3 | zig-deps/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 bootra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-fetch 2 | simple dependency management for zig projects! 3 | 4 | ## intro 5 | zig-fetch is a way to handle fetching dependencies for your project with: 6 | * no installation required 7 | * no submodules 8 | * no package config files 9 | 10 | the goal is to add some basic package management without having to change much about your zig workflow. 11 | 12 | for library developers, there are only a few changes needed to set up this workflow 13 | 14 | for library users, no change is needed - zig build will work just like normal! 15 | 16 | ## features 17 | zig-fetch provides the following features: 18 | 19 | * fetch and cache git repo dependencies 20 | * recursive fetch support - dependencies using zig-fetch will automatically fetch their dependencies recursively 21 | * add build steps and build options which get passed through to your build file 22 | 23 | ## getting started 24 | there are three simple steps to use zig-fetch 25 | 26 | 1. copy `fetch.zig` into your project folder 27 | 2. rename your `build.zig` to something different, like `compile.zig` 28 | 3. add a new `build.zig` where you define your project dependencies and call fetchAndBuild 29 | 30 | here's an example of a `build.zig` file: 31 | 32 | ``` 33 | const fetch = @import("fetch.zig"); 34 | const std = @import("std"); 35 | 36 | const deps = [_]fetch.Dependency{ 37 | .{ 38 | .name = "zig-fetch-example", 39 | .vcs = .{ 40 | .git = .{ 41 | .url = "https://github.com/bootradev/zig-fetch-example", 42 | .commit = "88548fb9f4ed307abd78d8d45bf590dcf9da17ed", 43 | .recursive = true, 44 | }, 45 | }, 46 | }, 47 | }; 48 | 49 | pub fn build(builder: *std.build.Builder) !void { 50 | fetch.addStep(builder, "example-step", "Test passing a step through build.zig"); 51 | fetch.addOption(builder, bool, "example-option", "Test passing an option through build.zig"); 52 | try fetch.fetchAndBuild(builder, "zig-deps", &deps, "compile.zig"); 53 | } 54 | 55 | ``` 56 | 57 | `fetchAndBuild` takes 4 arguments: 58 | 1. the builder 59 | 2. the name of the directory where dependencies are fetched to 60 | 3. array of dependencies to fetch 61 | 4. the name of the build file to call after fetching is complete 62 | 63 | ## build options 64 | (use `zig build --help` to see all available build options) 65 | 66 | * `fetch-skip` - Skip fetching dependencies entirely 67 | * `fetch-only` - Only fetch dependencies, do not build 68 | * `fetch-force` - Force fetch dependencies, even if already cached 69 | * `--verbose` - not a zig-fetch specific option, but this will add additional logging during the build 70 | 71 | ## additional notes 72 | 73 | see `build.zig` and `compile.zig` in this repo for an example of the workflow 74 | 75 | you can also check out https://github.com/bootradev/zig-fetch-example as additional reference 76 | 77 | ## credits 78 | 79 | thanks to https://github.com/desttinghim for coming up with idea for a separate build file! 80 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | // this file is an example of what a build.zig file using zig-fetch might look like 2 | 3 | const fetch = @import("fetch.zig"); 4 | const std = @import("std"); 5 | 6 | const deps = [_]fetch.Dependency{ 7 | .{ 8 | .name = "zig-fetch-example", 9 | .vcs = .{ 10 | .git = .{ 11 | .url = "https://github.com/bootradev/zig-fetch-example", 12 | .commit = "88548fb9f4ed307abd78d8d45bf590dcf9da17ed", 13 | .recursive = true, 14 | }, 15 | }, 16 | .recursive = true, 17 | }, 18 | }; 19 | 20 | pub fn build(builder: *std.build.Builder) !void { 21 | fetch.addStep(builder, "example-step", "Test passing a step through build.zig"); 22 | fetch.addOption(builder, bool, "example-option", "Test passing an option through build.zig"); 23 | try fetch.fetchAndBuild(builder, "zig-deps", &deps, "compile.zig"); 24 | } 25 | -------------------------------------------------------------------------------- /compile.zig: -------------------------------------------------------------------------------- 1 | // this file is an example of what a build file using zig-fetch might look like 2 | // make sure the parent import directory matches what is passed into fetchAndBuild 3 | 4 | const std = @import("std"); 5 | const zig_fetch_example = @import("zig-deps/zig-fetch-example/build.zig"); 6 | 7 | pub fn build(builder: *std.build.Builder) !void { 8 | std.log.info("the magic number is {}!", .{zig_fetch_example.magic_number}); 9 | 10 | const example_step = builder.step("example-step", "test passing a step through build.zig"); 11 | example_step.dependOn(&(try ExampleStep.init(builder)).step); 12 | 13 | const example_option = builder.option( 14 | bool, 15 | "example-option", 16 | "test passing an option through build.zig", 17 | ); 18 | std.log.info("example-option: {}", .{example_option}); 19 | } 20 | 21 | const ExampleStep = struct { 22 | step: std.build.Step, 23 | 24 | pub fn init(builder: *std.build.Builder) !*ExampleStep { 25 | var example = try builder.allocator.create(ExampleStep); 26 | example.* = .{ 27 | .step = std.build.Step.init(.custom, "example", builder.allocator, make), 28 | }; 29 | return example; 30 | } 31 | 32 | pub fn make(_: *std.build.Step) !void { 33 | std.log.info("Running example step!", .{}); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /fetch.zig: -------------------------------------------------------------------------------- 1 | // fetch.zig - a dependency management solution for zig projects! 2 | // see the repo at https://github.com/bootradev/zig-fetch for more info 3 | 4 | const std = @import("std"); 5 | 6 | // adds a step that will be passed through to the build file 7 | pub fn addStep( 8 | builder: *std.build.Builder, 9 | name: []const u8, 10 | description: []const u8, 11 | ) void { 12 | builder.step(name, description).dependOn(builder.getInstallStep()); 13 | } 14 | 15 | // adds an option that will be passed through to the build file 16 | pub fn addOption( 17 | builder: *std.build.Builder, 18 | comptime T: type, 19 | name: []const u8, 20 | description: []const u8, 21 | ) void { 22 | _ = builder.option(T, name, description); 23 | } 24 | 25 | pub const GitDependency = struct { 26 | url: []const u8, 27 | commit: []const u8, 28 | recursive: bool = false, // set to true to have this dependency fetch git submodules 29 | }; 30 | 31 | pub const Dependency = struct { 32 | name: []const u8, 33 | recursive: bool = false, // set to true when the dependency also uses zig-fetch 34 | vcs: union(enum) { 35 | git: GitDependency, 36 | }, 37 | }; 38 | 39 | pub fn fetchAndBuild( 40 | builder: *std.build.Builder, 41 | deps_dir: []const u8, 42 | deps: []const Dependency, 43 | build_file: []const u8, 44 | ) !void { 45 | // no-op standard options to pass through to build file 46 | _ = builder.standardTargetOptions(.{}); 47 | _ = builder.standardReleaseOptions(); 48 | 49 | const fetch_and_build = try FetchAndBuild.init(builder, deps_dir, deps, build_file); 50 | builder.getInstallStep().dependOn(&fetch_and_build.step); 51 | } 52 | 53 | const FetchAndBuild = struct { 54 | builder: *std.build.Builder, 55 | step: std.build.Step, 56 | deps: []const Dependency, 57 | build_file: []const u8, 58 | write_fetch_cache: bool, 59 | run_zig_build: bool, 60 | fetch_cache_path: []const u8, 61 | 62 | fn init( 63 | builder: *std.build.Builder, 64 | deps_dir: []const u8, 65 | deps: []const Dependency, 66 | build_file: []const u8, 67 | ) !*FetchAndBuild { 68 | const fetch_skip = builder.option( 69 | bool, 70 | "fetch-skip", 71 | "Skip fetch dependencies", 72 | ) orelse false; 73 | 74 | const fetch_only = builder.option( 75 | bool, 76 | "fetch-only", 77 | "Only fetch dependencies", 78 | ) orelse false; 79 | 80 | const fetch_force = builder.option( 81 | bool, 82 | "fetch-force", 83 | "Force fetch dependencies", 84 | ) orelse false; 85 | 86 | if (fetch_skip and fetch_only) { 87 | std.log.err("fetch-skip and fetch-only are mutually exclusive!", .{}); 88 | return error.InvalidOptions; 89 | } 90 | 91 | var fetch_and_build = try builder.allocator.create(FetchAndBuild); 92 | fetch_and_build.* = .{ 93 | .builder = builder, 94 | .step = std.build.Step.init(.custom, "fetch and build", builder.allocator, make), 95 | .deps = try builder.allocator.dupe(Dependency, deps), 96 | .build_file = builder.dupe(build_file), 97 | .write_fetch_cache = false, 98 | .run_zig_build = !fetch_only, 99 | .fetch_cache_path = getFetchCachePath(builder, deps_dir), 100 | }; 101 | 102 | const git_available = checkGitAvailable(builder); 103 | 104 | if (!fetch_skip) { 105 | const fetch_cache = try readFetchCache(builder, fetch_and_build.fetch_cache_path); 106 | 107 | for (deps) |dep| { 108 | if (!fetch_force) { 109 | if (fetch_cache) |cache| { 110 | var dep_in_cache = false; 111 | for (cache) |cache_dep| { 112 | if (dependencyEql(dep, cache_dep)) { 113 | dep_in_cache = true; 114 | break; 115 | } 116 | } 117 | if (dep_in_cache) { 118 | continue; 119 | } 120 | } 121 | } 122 | 123 | const fetch_dir = builder.pathJoin(&.{ builder.build_root, deps_dir, dep.name }); 124 | var fetch_step = &fetch_and_build.step; 125 | if (dep.recursive) { 126 | const recursive = try RecursiveFetch.init(builder, fetch_dir, fetch_force); 127 | fetch_step.dependOn(&recursive.step); 128 | fetch_step = &recursive.step; 129 | } 130 | 131 | switch (dep.vcs) { 132 | .git => |git_dep| { 133 | if (!git_available) { 134 | return error.GitNotAvailable; 135 | } 136 | const git_fetch = try GitFetch.init(builder, fetch_dir, git_dep); 137 | fetch_step.dependOn(&git_fetch.step); 138 | }, 139 | } 140 | 141 | fetch_and_build.write_fetch_cache = true; 142 | } 143 | } 144 | 145 | return fetch_and_build; 146 | } 147 | 148 | fn make(step: *std.build.Step) !void { 149 | const fetch_and_build = @fieldParentPtr(FetchAndBuild, "step", step); 150 | const builder = fetch_and_build.builder; 151 | 152 | if (fetch_and_build.write_fetch_cache) { 153 | try writeFetchCache(fetch_and_build.fetch_cache_path, fetch_and_build.deps); 154 | } 155 | 156 | if (fetch_and_build.run_zig_build) { 157 | const args = try std.process.argsAlloc(builder.allocator); 158 | defer std.process.argsFree(builder.allocator, args); 159 | 160 | // TODO: this might be platform specific. 161 | // on windows, 5 args are prepended before the user defined args 162 | const args_offset = 5; 163 | 164 | var build_args_list = std.ArrayList([]const u8).init(builder.allocator); 165 | defer build_args_list.deinit(); 166 | 167 | try build_args_list.appendSlice( 168 | &.{ "zig", "build", "--build-file", fetch_and_build.build_file }, 169 | ); 170 | for (args[args_offset..]) |arg| { 171 | if (std.mem.startsWith(u8, arg, "-Dfetch-skip=") or 172 | std.mem.startsWith(u8, arg, "-Dfetch-only=") or 173 | std.mem.startsWith(u8, arg, "-Dfetch-force=")) 174 | { 175 | continue; 176 | } 177 | try build_args_list.append(arg); 178 | } 179 | 180 | if (fetch_and_build.write_fetch_cache or builder.verbose) { 181 | std.log.info("building with build file {s}...", .{fetch_and_build.build_file}); 182 | } 183 | 184 | const build_args = build_args_list.items; 185 | runChildProcess(builder, builder.build_root, build_args, true) catch return; 186 | } 187 | } 188 | }; 189 | 190 | fn getFetchCachePath(builder: *std.build.Builder, deps_dir: []const u8) []const u8 { 191 | return builder.pathJoin(&.{ builder.build_root, deps_dir, "fetch_cache" }); 192 | } 193 | 194 | fn readFetchCache(builder: *std.build.Builder, cache_path: []const u8) !?[]const Dependency { 195 | const cache_file = std.fs.cwd().openFile(cache_path, .{}) catch return null; 196 | defer cache_file.close(); 197 | const reader = cache_file.reader(); 198 | 199 | var dependencies = std.ArrayList(Dependency).init(builder.allocator); 200 | var read_buf: [256]u8 = undefined; 201 | while (true) { 202 | const name = builder.dupe(reader.readUntilDelimiter(&read_buf, '\n') catch |e| { 203 | if (e == error.EndOfStream) { 204 | break; 205 | } else { 206 | return e; 207 | } 208 | }); 209 | 210 | var dependency: Dependency = undefined; 211 | dependency.name = name; 212 | 213 | const vcs_type = try reader.readUntilDelimiter(&read_buf, '\n'); 214 | if (std.mem.eql(u8, vcs_type, "git")) { 215 | const url = builder.dupe(try reader.readUntilDelimiter(&read_buf, '\n')); 216 | const commit = builder.dupe(try reader.readUntilDelimiter(&read_buf, '\n')); 217 | const recursive = try parseBool(try reader.readUntilDelimiter(&read_buf, '\n')); 218 | dependency.vcs = .{ 219 | .git = .{ 220 | .url = url, 221 | .commit = commit, 222 | .recursive = recursive, 223 | }, 224 | }; 225 | } else { 226 | return error.InvalidVcsType; 227 | } 228 | 229 | try dependencies.append(dependency); 230 | } 231 | 232 | return dependencies.toOwnedSlice(); 233 | } 234 | 235 | fn writeFetchCache(cache_path: []const u8, deps: []const Dependency) !void { 236 | try std.fs.cwd().makePath(std.fs.path.dirname(cache_path) orelse unreachable); 237 | 238 | const cache_file = try std.fs.cwd().createFile(cache_path, .{}); 239 | const writer = cache_file.writer(); 240 | 241 | for (deps) |dep| { 242 | try writer.print("{s}\n", .{dep.name}); 243 | switch (dep.vcs) { 244 | .git => |git_dep| { 245 | try writer.print("git\n", .{}); 246 | try writer.print("{s}\n", .{git_dep.url}); 247 | try writer.print("{s}\n", .{git_dep.commit}); 248 | try writer.print("{}\n", .{git_dep.recursive}); 249 | }, 250 | } 251 | } 252 | } 253 | 254 | const RecursiveFetch = struct { 255 | builder: *std.build.Builder, 256 | step: std.build.Step, 257 | dir: []const u8, 258 | fetch_force: bool, 259 | 260 | pub fn init( 261 | builder: *std.build.Builder, 262 | dir: []const u8, 263 | fetch_force: bool, 264 | ) !*RecursiveFetch { 265 | var recursive_fetch = try builder.allocator.create(RecursiveFetch); 266 | recursive_fetch.* = .{ 267 | .builder = builder, 268 | .step = std.build.Step.init(.custom, "recursive fetch", builder.allocator, make), 269 | .dir = dir, 270 | .fetch_force = fetch_force, 271 | }; 272 | return recursive_fetch; 273 | } 274 | 275 | pub fn make(step: *std.build.Step) !void { 276 | const recursive_fetch = @fieldParentPtr(RecursiveFetch, "step", step); 277 | const builder = recursive_fetch.builder; 278 | 279 | var dir = try std.fs.openDirAbsolute(recursive_fetch.dir, .{}); 280 | defer dir.close(); 281 | if (dir.openFile("build.zig", .{})) |file| { 282 | file.close(); 283 | 284 | std.log.info("recursively fetching within {s}...", .{recursive_fetch.dir}); 285 | 286 | var build_args_list = std.ArrayList([]const u8).init(builder.allocator); 287 | defer build_args_list.deinit(); 288 | try build_args_list.appendSlice(&.{ "zig", "build", "-Dfetch-only=true" }); 289 | if (builder.verbose) { 290 | try build_args_list.append("--verbose"); 291 | } 292 | if (recursive_fetch.fetch_force) { 293 | try build_args_list.append("-Dfetch-force=true"); 294 | } 295 | 296 | const build_args = build_args_list.items; 297 | try runChildProcess(builder, recursive_fetch.dir, build_args, true); 298 | } else |_| {} 299 | } 300 | }; 301 | 302 | const GitFetch = struct { 303 | builder: *std.build.Builder, 304 | step: std.build.Step, 305 | dep: GitDependency, 306 | dir: []const u8, 307 | 308 | pub fn init( 309 | builder: *std.build.Builder, 310 | dir: []const u8, 311 | dep: GitDependency, 312 | ) !*GitFetch { 313 | var git_fetch = try builder.allocator.create(GitFetch); 314 | git_fetch.* = .{ 315 | .builder = builder, 316 | .step = std.build.Step.init(.custom, "git fetch", builder.allocator, make), 317 | .dep = dep, 318 | .dir = dir, 319 | }; 320 | return git_fetch; 321 | } 322 | 323 | pub fn make(step: *std.build.Step) !void { 324 | const git_fetch = @fieldParentPtr(GitFetch, "step", step); 325 | const builder = git_fetch.builder; 326 | 327 | std.log.info("fetching from git into {s}...", .{git_fetch.dir}); 328 | 329 | var dir_exists = true; 330 | std.fs.accessAbsolute(git_fetch.dir, .{}) catch { 331 | dir_exists = false; 332 | }; 333 | 334 | if (dir_exists) { 335 | const fetch_args = &.{ "git", "fetch" }; 336 | try runChildProcess(builder, builder.build_root, fetch_args, builder.verbose); 337 | } else { 338 | const clone_args = &.{ "git", "clone", git_fetch.dep.url, git_fetch.dir }; 339 | try runChildProcess(builder, builder.build_root, clone_args, builder.verbose); 340 | } 341 | 342 | if (git_fetch.dep.recursive) { 343 | const submodule_args = &.{ "git", "submodule", "update", "--init", "--recursive" }; 344 | try runChildProcess(builder, git_fetch.dir, submodule_args, builder.verbose); 345 | } 346 | 347 | const checkout_args = &.{ "git", "checkout", git_fetch.dep.commit }; 348 | try runChildProcess(builder, git_fetch.dir, checkout_args, builder.verbose); 349 | } 350 | }; 351 | 352 | fn checkGitAvailable(builder: *std.build.Builder) bool { 353 | const args = &.{ "git", "--version" }; 354 | runChildProcess(builder, builder.build_root, args, builder.verbose) catch return false; 355 | return true; 356 | } 357 | 358 | fn runChildProcess( 359 | builder: *std.build.Builder, 360 | cwd: []const u8, 361 | args: []const []const u8, 362 | log_output: bool, 363 | ) !void { 364 | try logCommand(builder, args); 365 | 366 | const result = try std.ChildProcess.exec(.{ 367 | .allocator = builder.allocator, 368 | .argv = args, 369 | .cwd = cwd, 370 | .env_map = builder.env_map, 371 | }); 372 | defer builder.allocator.free(result.stdout); 373 | defer builder.allocator.free(result.stderr); 374 | 375 | const err = result.term != .Exited or result.term.Exited != 0; 376 | if (log_output or err) { 377 | try std.io.getStdOut().writer().writeAll(result.stdout); 378 | try std.io.getStdErr().writer().writeAll(result.stderr); 379 | } 380 | 381 | if (err) { 382 | return error.RunChildProcessFailed; 383 | } 384 | } 385 | 386 | fn logCommand(builder: *std.build.Builder, args: []const []const u8) !void { 387 | if (builder.verbose) { 388 | var command = std.ArrayList(u8).init(builder.allocator); 389 | defer command.deinit(); 390 | 391 | try command.appendSlice("RUNNING COMMAND:"); 392 | for (args) |arg| { 393 | try command.append(' '); 394 | try command.appendSlice(arg); 395 | } 396 | 397 | std.log.info("{s}", .{command.items}); 398 | } 399 | } 400 | 401 | pub fn dependencyEql(a: Dependency, b: Dependency) bool { 402 | return std.mem.eql(u8, a.name, b.name) and 403 | a.recursive == b.recursive and 404 | std.meta.activeTag(a.vcs) == std.meta.activeTag(b.vcs) and 405 | switch (a.vcs) { 406 | .git => std.mem.eql(u8, a.vcs.git.url, b.vcs.git.url) and 407 | std.mem.eql(u8, a.vcs.git.commit, b.vcs.git.commit) and 408 | a.vcs.git.recursive == b.vcs.git.recursive, 409 | }; 410 | } 411 | 412 | fn parseBool(str: []const u8) !bool { 413 | if (std.mem.eql(u8, str, "true")) { 414 | return true; 415 | } else if (std.mem.eql(u8, str, "false")) { 416 | return false; 417 | } else { 418 | return error.ParseBoolFailed; 419 | } 420 | } 421 | --------------------------------------------------------------------------------