GitRepoStep.zig 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. //! Publish Date: 2021_10_17
  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 (
  55. std.fs.path.resolve(b.allocator, &[_][]const u8{
  56. b.build_root,
  57. "dep",
  58. name,
  59. })
  60. ) catch @panic("memory"),
  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) !void {
  78. const self = @fieldParentPtr(GitRepoStep, "step", step);
  79. std.fs.accessAbsolute(self.path, std.fs.File.OpenFlags { .read = true }) catch {
  80. const branch_args = if (self.branch) |b| &[2][]const u8 {" -b ", b} else &[2][]const u8 {"", ""};
  81. if (!self.fetch_enabled) {
  82. std.debug.print("Error: git repository '{s}' does not exist\n", .{self.path});
  83. std.debug.print(" Use -Dfetch to download it automatically, or run the following to clone it:\n", .{});
  84. std.debug.print(" git clone {s}{s}{s} {s} && git -C {3s} checkout {s} -b for_ziget\n",
  85. .{self.url, branch_args[0], branch_args[1], self.path, self.sha});
  86. std.os.exit(1);
  87. }
  88. {
  89. var args = std.ArrayList([]const u8).init(self.builder.allocator);
  90. defer args.deinit();
  91. try args.append("git");
  92. try args.append("clone");
  93. try args.append(self.url);
  94. // TODO: clone it to a temporary location in case of failure
  95. // also, remove that temporary location before running
  96. try args.append(self.path);
  97. if (self.branch) |branch| {
  98. try args.append("-b");
  99. try args.append(branch);
  100. }
  101. try run(self.builder, args.items);
  102. }
  103. try run(self.builder, &[_][]const u8 {
  104. "git",
  105. "-C", self.path,
  106. "checkout",
  107. self.sha,
  108. "-b",
  109. "fordep",
  110. });
  111. };
  112. try self.checkSha();
  113. }
  114. fn checkSha(self: GitRepoStep) !void {
  115. if (self.sha_check == .none)
  116. return;
  117. const result: union(enum) { failed: anyerror, output: []const u8 } = blk: {
  118. const result = std.ChildProcess.exec(.{
  119. .allocator = self.builder.allocator,
  120. .argv = &[_][]const u8 {
  121. "git",
  122. "-C", self.path,
  123. "rev-parse",
  124. "HEAD",
  125. },
  126. .cwd = self.builder.build_root,
  127. .env_map = self.builder.env_map,
  128. }) catch |e| break :blk .{ .failed = e };
  129. try std.io.getStdErr().writer().writeAll(result.stderr);
  130. switch (result.term) {
  131. .Exited => |code| {
  132. if (code == 0) break :blk .{ .output = result.stdout };
  133. break :blk .{ .failed = error.GitProcessNonZeroExit };
  134. },
  135. .Signal => break :blk .{ .failed = error.GitProcessFailedWithSignal },
  136. .Stopped => break :blk .{ .failed = error.GitProcessWasStopped },
  137. .Unknown => break :blk .{ .failed = error.GitProcessFailed },
  138. }
  139. };
  140. switch (result) {
  141. .failed => |err| {
  142. return self.sha_check.reportFail("failed to retreive sha for repository '{s}': {s}", .{self.name, @errorName(err)});
  143. },
  144. .output => |output| {
  145. if (!std.mem.eql(u8, std.mem.trimRight(u8, output, "\n\r"), self.sha)) {
  146. return self.sha_check.reportFail("repository '{s}' sha does not match\nexpected: {s}\nactual : {s}\n", .{self.name, self.sha, output});
  147. }
  148. },
  149. }
  150. }
  151. fn run(builder: *std.build.Builder, argv: []const []const u8) !void {
  152. {
  153. var msg = std.ArrayList(u8).init(builder.allocator);
  154. defer msg.deinit();
  155. const writer = msg.writer();
  156. var prefix: []const u8 = "";
  157. for (argv) |arg| {
  158. try writer.print("{s}\"{s}\"", .{prefix, arg});
  159. prefix = " ";
  160. }
  161. std.log.info("[RUN] {s}", .{msg.items});
  162. }
  163. const child = try std.ChildProcess.init(argv, builder.allocator);
  164. defer child.deinit();
  165. child.stdin_behavior = .Ignore;
  166. child.stdout_behavior = .Inherit;
  167. child.stderr_behavior = .Inherit;
  168. child.cwd = builder.build_root;
  169. child.env_map = builder.env_map;
  170. try child.spawn();
  171. const result = try child.wait();
  172. switch (result) {
  173. .Exited => |code| if (code != 0) {
  174. std.log.err("git clone failed with exit code {}", .{code});
  175. std.os.exit(0xff);
  176. },
  177. else => {
  178. std.log.err("git clone failed with: {}", .{result});
  179. std.os.exit(0xff);
  180. },
  181. }
  182. }
  183. // Get's the repository path and also verifies that the step requesting the path
  184. // is dependent on this step.
  185. pub fn getPath(self: *const GitRepoStep, who_wants_to_know: *const std.build.Step) []const u8 {
  186. if (!hasDependency(who_wants_to_know, &self.step))
  187. @panic("a step called GitRepoStep.getPath but has not added it as a dependency");
  188. return self.path;
  189. }