GitRepoStep.zig 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. //! Publish Date: 2023_03_19
  2. //! This file is hosted at github.com/marler8997/zig-build-repos and is meant to be copied
  3. //! to projects that use it.
  4. const std = @import("std");
  5. const GitRepoStep = @This();
  6. pub const ShaCheck = enum {
  7. none,
  8. warn,
  9. err,
  10. pub fn reportFail(self: ShaCheck, comptime fmt: []const u8, args: anytype) void {
  11. switch (self) {
  12. .none => unreachable,
  13. .warn => std.log.warn(fmt, args),
  14. .err => {
  15. std.log.err(fmt, args);
  16. std.os.exit(0xff);
  17. },
  18. }
  19. }
  20. };
  21. step: std.build.Step,
  22. url: []const u8,
  23. name: []const u8,
  24. branch: ?[]const u8 = null,
  25. sha: []const u8,
  26. path: []const u8,
  27. sha_check: ShaCheck = .warn,
  28. fetch_enabled: bool,
  29. var cached_default_fetch_option: ?bool = null;
  30. pub fn defaultFetchOption(b: *std.build.Builder) bool {
  31. if (cached_default_fetch_option) |_| {} else {
  32. cached_default_fetch_option = if (b.option(bool, "fetch", "automatically fetch network resources")) |o| o else false;
  33. }
  34. return cached_default_fetch_option.?;
  35. }
  36. pub fn create(b: *std.build.Builder, opt: struct {
  37. url: []const u8,
  38. branch: ?[]const u8 = null,
  39. sha: []const u8,
  40. path: ?[]const u8 = null,
  41. sha_check: ShaCheck = .warn,
  42. fetch_enabled: ?bool = null,
  43. first_ret_addr: ?usize = null,
  44. }) *GitRepoStep {
  45. var result = b.allocator.create(GitRepoStep) catch @panic("memory");
  46. const name = std.fs.path.basename(opt.url);
  47. result.* = GitRepoStep{
  48. .step = std.build.Step.init(.{
  49. .id = .custom,
  50. .name = b.fmt("clone git repository '{s}'", .{name}),
  51. .owner = b,
  52. .makeFn = make,
  53. .first_ret_addr = opt.first_ret_addr orelse @returnAddress(),
  54. .max_rss = 0,
  55. }),
  56. .url = opt.url,
  57. .name = name,
  58. .branch = opt.branch,
  59. .sha = opt.sha,
  60. .path = if (opt.path) |p| b.allocator.dupe(u8, p) catch @panic("OOM") else b.pathFromRoot(b.pathJoin(&.{ "dep", name })),
  61. .sha_check = opt.sha_check,
  62. .fetch_enabled = if (opt.fetch_enabled) |fe| fe else defaultFetchOption(b),
  63. };
  64. return result;
  65. }
  66. // TODO: this should be included in std.build, it helps find bugs in build files
  67. fn hasDependency(step: *const std.build.Step, dep_candidate: *const std.build.Step) bool {
  68. for (step.dependencies.items) |dep| {
  69. // TODO: should probably use step.loop_flag to prevent infinite recursion
  70. // when a circular reference is encountered, or maybe keep track of
  71. // the steps encounterd with a hash set
  72. if (dep == dep_candidate or hasDependency(dep, dep_candidate))
  73. return true;
  74. }
  75. return false;
  76. }
  77. fn make(step: *std.Build.Step, prog_node: *std.Progress.Node) !void {
  78. _ = prog_node;
  79. const self = @fieldParentPtr(GitRepoStep, "step", step);
  80. std.fs.accessAbsolute(self.path, .{}) catch {
  81. const branch_args = if (self.branch) |b| &[2][]const u8{ " -b ", b } else &[2][]const u8{ "", "" };
  82. if (!self.fetch_enabled) {
  83. std.debug.print("Error: git repository '{s}' does not exist\n", .{self.path});
  84. std.debug.print(" Use -Dfetch to download it automatically, or run the following to clone it:\n", .{});
  85. std.debug.print(" git clone {s}{s}{s} {s} && git -C {3s} checkout {s} -b fordep\n", .{
  86. self.url,
  87. branch_args[0],
  88. branch_args[1],
  89. self.path,
  90. self.sha,
  91. });
  92. std.os.exit(1);
  93. }
  94. {
  95. var args = std.ArrayList([]const u8).init(self.step.owner.allocator);
  96. defer args.deinit();
  97. try args.append("git");
  98. try args.append("clone");
  99. try args.append(self.url);
  100. // TODO: clone it to a temporary location in case of failure
  101. // also, remove that temporary location before running
  102. try args.append(self.path);
  103. if (self.branch) |branch| {
  104. try args.append("-b");
  105. try args.append(branch);
  106. }
  107. try run(self.step.owner, args.items);
  108. }
  109. try run(self.step.owner, &[_][]const u8{
  110. "git",
  111. "-C",
  112. self.path,
  113. "checkout",
  114. self.sha,
  115. "-b",
  116. "fordep",
  117. });
  118. };
  119. try self.checkSha();
  120. }
  121. fn checkSha(self: GitRepoStep) !void {
  122. if (self.sha_check == .none)
  123. return;
  124. const result: union(enum) { failed: anyerror, output: []const u8 } = blk: {
  125. const result = std.ChildProcess.exec(.{
  126. .allocator = self.step.owner.allocator,
  127. .argv = &[_][]const u8{
  128. "git",
  129. "-C",
  130. self.path,
  131. "rev-parse",
  132. "HEAD",
  133. },
  134. .cwd = self.step.owner.build_root.path,
  135. .env_map = self.step.owner.env_map,
  136. }) catch |e| break :blk .{ .failed = e };
  137. try std.io.getStdErr().writer().writeAll(result.stderr);
  138. switch (result.term) {
  139. .Exited => |code| {
  140. if (code == 0) break :blk .{ .output = result.stdout };
  141. break :blk .{ .failed = error.GitProcessNonZeroExit };
  142. },
  143. .Signal => break :blk .{ .failed = error.GitProcessFailedWithSignal },
  144. .Stopped => break :blk .{ .failed = error.GitProcessWasStopped },
  145. .Unknown => break :blk .{ .failed = error.GitProcessFailed },
  146. }
  147. };
  148. switch (result) {
  149. .failed => |err| {
  150. return self.sha_check.reportFail("failed to retreive sha for repository '{s}': {s}", .{ self.name, @errorName(err) });
  151. },
  152. .output => |output| {
  153. if (!std.mem.eql(u8, std.mem.trimRight(u8, output, "\n\r"), self.sha)) {
  154. return self.sha_check.reportFail("repository '{s}' sha does not match\nexpected: {s}\nactual : {s}\n", .{ self.name, self.sha, output });
  155. }
  156. },
  157. }
  158. }
  159. fn run(builder: *std.build.Builder, argv: []const []const u8) !void {
  160. {
  161. var msg = std.ArrayList(u8).init(builder.allocator);
  162. defer msg.deinit();
  163. const writer = msg.writer();
  164. var prefix: []const u8 = "";
  165. for (argv) |arg| {
  166. try writer.print("{s}\"{s}\"", .{ prefix, arg });
  167. prefix = " ";
  168. }
  169. std.log.info("[RUN] {s}", .{msg.items});
  170. }
  171. var child = std.ChildProcess.init(argv, builder.allocator);
  172. child.stdin_behavior = .Ignore;
  173. child.stdout_behavior = .Inherit;
  174. child.stderr_behavior = .Inherit;
  175. child.cwd = builder.build_root.path;
  176. child.env_map = builder.env_map;
  177. try child.spawn();
  178. const result = try child.wait();
  179. switch (result) {
  180. .Exited => |code| if (code != 0) {
  181. std.log.err("git clone failed with exit code {}", .{code});
  182. std.os.exit(0xff);
  183. },
  184. else => {
  185. std.log.err("git clone failed with: {}", .{result});
  186. std.os.exit(0xff);
  187. },
  188. }
  189. }
  190. // Get's the repository path and also verifies that the step requesting the path
  191. // is dependent on this step.
  192. pub fn getPath(self: *const GitRepoStep, who_wants_to_know: *const std.build.Step) []const u8 {
  193. if (!hasDependency(who_wants_to_know, &self.step))
  194. @panic("a step called GitRepoStep.getPath but has not added it as a dependency");
  195. return self.path;
  196. }