GitRepoStep.zig 7.2 KB

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