From ae6cf5fbe23993d09f9e99ed80fc6717f25f6317 Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Sat, 17 Sep 2022 13:13:34 -0700 Subject: [PATCH] cmd: add the `generate` command --- docs/commands/generate.md | 33 ++++ docs/deps.zig.md | 6 + src/cmd/generate.zig | 375 ++++++++++++++++++++++++++++++++++++++ src/lib.zig | 1 + src/util/module.zig | 20 ++ 5 files changed, 435 insertions(+) create mode 100644 docs/commands/generate.md create mode 100644 src/cmd/generate.zig diff --git a/docs/commands/generate.md b/docs/commands/generate.md new file mode 100644 index 0000000000000000000000000000000000000000..b297985f50676da3ca7d15966721dbb2a1b75c97 --- /dev/null +++ b/docs/commands/generate.md @@ -0,0 +1,33 @@ +## `generate` command +``` +zigmod generate +``` + +- This command takes no parameters and will generate a `deps.zig` in the root of your project. This is the file that you will then import into your `build.zig` to automatically add all the necessary packages and (any) C code that may be in your dependencies. +- This behavior is similar to [`zigmod fetch`](./fetch.md) but it does the fetching of dependencies in `deps.zig` itself to enable your users to only need `zig build` (and optionally `git`). +- You as an program author will still need `zigmod` installed during development, but consumers of your app can get started with only a `git clone` and `zig build`! +- To this end, a `deps.zig` made by running `zigmod generate` *will* want to be checked into source control. Don't forget to remove it from `.gitignore`. +- The version of dependencies fetched by `deps.zig` from this command is based on your `zigmod.lock` so no surprise breaks from users. + +For a full reference on the fields available in `deps.zig` you can check [here](../deps.zig.md). + +### Adding `deps.zig` to your `build.zig` +```diff + const std = @import("std"); ++const deps = @import("./deps.zig"); + + pub fn build(b: *std.build.Builder) void { + const target = b.standardTargetOptions(.{}); + + const mode = b.standardReleaseOptions(); + + const exe = b.addExecutable("hello", "src/main.zig"); + exe.setTarget(target); + exe.setBuildMode(mode); ++ deps.addAllTo(exe); + exe.install(); +``` + +If you don't want Zigmod to handle adding packages to your project and only do resource fetching then `deps.fetch(exe)` may be called independently of `deps.addAllTo(exe)`. + +`deps.addAllTo(exe)` will call `deps.fetch(exe)` inside of it, so as to be compatible with `deps.zig` generated by `zigmod fetch`. diff --git a/docs/deps.zig.md b/docs/deps.zig.md index effbd1e969b815dd0845382790c4e0608a6deaeb..9c6848e46fa60db563e8302f565eba731af34315 100644 --- a/docs/deps.zig.md +++ b/docs/deps.zig.md @@ -4,6 +4,12 @@ This file is generated by [`zigmod fetch`](commands/fetch.md) or [`zigmod ci`](c ### `cache` - Type: `[]const u8` The relative path root to which all dependency sources are saved. Currently always `.zigmod/deps`. +> Note: only available when made with `zigmod fetch`. + +### `fetch` +- Type: `pub fn (exe: *std.build.LibExeObjStep) void` +A helper function to automatically pull in dependencies, purely from the `zig build` system without the need to have `zigmod` installed. +> Note: only available when made with `zigmod generate`. ### `addAllTo` - Type: `pub fn (exe: *std.build.LibExeObjStep) void` diff --git a/src/cmd/generate.zig b/src/cmd/generate.zig new file mode 100644 index 0000000000000000000000000000000000000000..0a8078ae42791ea148ef9a483746935df4ee3eeb --- /dev/null +++ b/src/cmd/generate.zig @@ -0,0 +1,375 @@ +const std = @import("std"); +const string = []const u8; + +const zigmod = @import("../lib.zig"); +const u = @import("./../util/index.zig"); +const common = @import("./../common.zig"); + +// +// + +pub fn execute(args: [][]u8) !void { + _ = args; + + // + const gpa = std.heap.c_allocator; + const cachepath = try std.fs.path.join(gpa, &.{ ".zigmod", "deps" }); + const dir = std.fs.cwd(); + + var options = common.CollectOptions{ + .log = false, + .update = false, + .alloc = gpa, + }; + const top_module = try common.collect_deps_deep(cachepath, dir, &options); + + var list = std.ArrayList(zigmod.Module).init(gpa); + try common.collect_pkgs(top_module, &list); + + std.sort.sort(zigmod.Module, list.items, {}, zigmod.Module.lessThan); + + try create_depszig(gpa, cachepath, dir, top_module, list.items); +} + +pub fn create_depszig(alloc: std.mem.Allocator, cachepath: string, dir: std.fs.Dir, top_module: zigmod.Module, list: []const zigmod.Module) !void { + const f = try dir.createFile("deps.zig", .{}); + defer f.close(); + + const w = f.writer(); + try w.writeAll("// zig fmt: off\n"); + try w.writeAll("const std = @import(\"std\");\n"); + try w.writeAll("const builtin = @import(\"builtin\");\n"); + try w.writeAll("const Pkg = std.build.Pkg;\n"); + try w.writeAll("const string = []const u8;\n"); + try w.writeAll("\n"); + try w.writeAll( + \\pub const GitExactStep = struct { + \\ step: std.build.Step, + \\ builder: *std.build.Builder, + \\ url: string, + \\ commit: string, + \\ + \\ pub fn create(b: *std.build.Builder, url: string, commit: string) *GitExactStep { + \\ var result = b.allocator.create(GitExactStep) catch @panic("memory"); + \\ result.* = GitExactStep{ + \\ .step = std.build.Step.init(.custom, b.fmt("git clone {s} @ {s}", .{ url, commit }), b.allocator, make), + \\ .builder = b, + \\ .url = url, + \\ .commit = commit, + \\ }; + \\ + \\ var urlpath = url; + \\ urlpath = trimPrefix(u8, urlpath, "https://"); + \\ urlpath = trimPrefix(u8, urlpath, "git://"); + \\ const repopath = b.fmt("{s}/zigmod/deps/git/{s}/{s}", .{ b.cache_root, urlpath, commit }); + \\ flip(std.fs.cwd().access(repopath, .{})) catch return result; + \\ + \\ var clonestep = std.build.RunStep.create(b, "clone"); + \\ clonestep.addArgs(&.{ "git", "clone", "-q", "--progress", url, repopath }); + \\ result.step.dependOn(&clonestep.step); + \\ + \\ var checkoutstep = std.build.RunStep.create(b, "checkout"); + \\ checkoutstep.addArgs(&.{ "git", "-C", repopath, "checkout", "-q", commit }); + \\ result.step.dependOn(&checkoutstep.step); + \\ + \\ return result; + \\ } + \\ + \\ fn make(step: *std.build.Step) !void { + \\ _ = step; + \\ } + \\}; + \\ + \\pub fn fetch(exe: *std.build.LibExeObjStep) void { + \\ const b = exe.builder; + \\ inline for (comptime std.meta.declarations(package_data)) |decl| { + \\ const path = &@field(package_data, decl.name).entry; + \\ const root = if (@field(package_data, decl.name).store) |_| b.cache_root else "."; + \\ if (path.* != null) path.* = b.fmt("{s}/zigmod/deps{s}", .{ root, path.*.? }); + \\ } + \\ + ); + for (list) |module| { + switch (module.type) { + .local => {}, + .system_lib => {}, + .framework => {}, + .git => try w.print(" exe.step.dependOn(&GitExactStep.create(b, \"{s}\", \"{s}\").step);\n", .{ module.dep.?.path, try module.pin(alloc, cachepath) }), + .hg => @panic("TODO"), + .http => @panic("TODO"), + } + } + try w.writeAll( + \\} + \\ + \\fn trimPrefix(comptime T: type, haystack: []const T, needle: []const T) []const T { + \\ if (std.mem.startsWith(T, haystack, needle)) { + \\ return haystack[needle.len .. haystack.len]; + \\ } + \\ return haystack; + \\} + \\ + \\fn flip(foo: anytype) !void { + \\ _ = foo catch return; + \\ return error.ExpectedError; + \\} + \\ + \\pub fn addAllTo(exe: *std.build.LibExeObjStep) void { + \\ checkMinZig(builtin.zig_version, exe); + \\ fetch(exe); + \\ const b = exe.builder; + \\ @setEvalBranchQuota(1_000_000); + \\ for (packages) |pkg| { + \\ exe.addPackage(pkg.zp(b)); + \\ } + \\ var llc = false; + \\ var vcpkg = false; + \\ inline for (comptime std.meta.declarations(package_data)) |decl| { + \\ const pkg = @as(Package, @field(package_data, decl.name)); + \\ const root = if (pkg.store) |st| b.fmt("{s}/zigmod/deps/{s}", .{ b.cache_root, st }) else "."; + \\ for (pkg.system_libs) |item| { + \\ exe.linkSystemLibrary(item); + \\ llc = true; + \\ } + \\ for (pkg.frameworks) |item| { + \\ if (!builtin.target.isDarwin()) @panic(exe.builder.fmt("a dependency is attempting to link to the framework {s}, which is only possible under Darwin", .{item})); + \\ exe.linkFramework(item); + \\ llc = true; + \\ } + \\ for (pkg.c_include_dirs) |item| { + \\ exe.addIncludeDir(b.fmt("{s}/{s}", .{ root, item })); + \\ llc = true; + \\ } + \\ for (pkg.c_source_files) |item| { + \\ exe.addCSourceFile(b.fmt("{s}/{s}", .{ root, item }), pkg.c_source_flags); + \\ llc = true; + \\ } + \\ vcpkg = vcpkg or pkg.vcpkg; + \\ } + \\ if (llc) exe.linkLibC(); + \\ if (builtin.os.tag == .windows and vcpkg) exe.addVcpkgPaths(.static) catch |err| @panic(@errorName(err)); + \\} + \\ + \\pub const Package = struct { + \\ name: string = "", + \\ entry: ?string = null, + \\ store: ?string = null, + \\ deps: []const *Package = &.{}, + \\ c_include_dirs: []const string = &.{}, + \\ c_source_files: []const string = &.{}, + \\ c_source_flags: []const string = &.{}, + \\ system_libs: []const string = &.{}, + \\ frameworks: []const string = &.{}, + \\ vcpkg: bool = false, + \\ + \\ pub fn zp(self: *const Package, b: *std.build.Builder) Pkg { + \\ var temp: [100]Pkg = undefined; + \\ for (self.deps) |item, i| { + \\ temp[i] = item.zp(b); + \\ } + \\ return .{ + \\ .name = self.name, + \\ .source = .{ .path = self.entry.? }, + \\ .dependencies = b.allocator.dupe(Pkg, temp[0..self.deps.len]) catch @panic("oom"), + \\ }; + \\ } + \\}; + \\ + \\ + ); + + try w.print( + \\fn checkMinZig(current: std.SemanticVersion, exe: *std.build.LibExeObjStep) void {{ + \\ const min = std.SemanticVersion.parse("{?}") catch return; + \\ if (current.order(min).compare(.lt)) @panic(exe.builder.fmt("Your Zig version v{{}} does not meet the minimum build requirement of v{{}}", .{{current, min}})); + \\}} + \\ + \\ + , .{top_module.minZigVersion()}); + + try w.writeAll("pub const package_data = struct {\n"); + var duped = std.ArrayList(zigmod.Module).init(alloc); + for (list) |mod| { + if (mod.type == .system_lib or mod.type == .framework) { + continue; + } + try duped.append(mod); + } + try print_pkg_data_to(w, alloc, cachepath, &duped, &std.ArrayList(zigmod.Module).init(alloc)); + try w.writeAll("};\n\n"); + + try w.writeAll("pub const packages = "); + try print_deps(w, top_module); + try w.writeAll(";\n\n"); + + try w.writeAll("pub const pkgs = "); + try print_pkgs(alloc, w, top_module); + try w.writeAll(";\n\n"); + + try w.writeAll("pub const imports = struct {\n"); + try print_imports(alloc, w, top_module, cachepath); + try w.writeAll("};\n"); +} + +fn print_dirs(w: std.fs.File.Writer, list: []const zigmod.Module) !void { + for (list) |mod| { + if (mod.type == .system_lib or mod.type == .framework) continue; + if (std.mem.eql(u8, mod.id, "root")) { + try w.writeAll(" pub const _root = \"\";\n"); + continue; + } + try w.print(" pub const _{s} = cache ++ \"/{}\";\n", .{ mod.short_id(), std.zig.fmtEscapes(mod.clean_path) }); + } +} + +fn print_deps(w: std.fs.File.Writer, m: zigmod.Module) !void { + try w.writeAll("[_]*const Package{\n"); + for (m.deps) |d| { + if (d.main.len == 0) { + continue; + } + if (d.for_build) { + continue; + } + try w.print(" &package_data._{s},\n", .{d.id[0..12]}); + } + try w.writeAll("}"); +} + +fn print_pkg_data_to(w: std.fs.File.Writer, alloc: std.mem.Allocator, cachepath: string, notdone: *std.ArrayList(zigmod.Module), done: *std.ArrayList(zigmod.Module)) !void { + var len: usize = notdone.items.len; + while (notdone.items.len > 0) { + for (notdone.items) |mod, i| { + if (contains_all(mod.deps, done.items)) { + try w.print( + \\ pub var _{s} = Package{{ + \\ + , .{ + mod.short_id(), + }); + var fixed_path = if (std.mem.startsWith(u8, mod.clean_path, "v/")) mod.clean_path[2..std.mem.lastIndexOfScalar(u8, mod.clean_path, '/').?] else mod.clean_path; + switch (mod.type) { + .system_lib, .framework => {}, + .local => {}, + .git => try w.print(" .store = \"/{}/{s}\",\n", .{ std.zig.fmtEscapes(fixed_path), try mod.pin(alloc, cachepath) }), + .hg => @panic("TODO"), + .http => @panic("TODO"), + } + if (mod.main.len > 0 and !std.mem.eql(u8, mod.id, "root")) { + try w.print(" .name = \"{s}\",\n", .{mod.name}); + try w.print(" .entry = \"/{}/{s}/{s}\",\n", .{ std.zig.fmtEscapes(fixed_path), try mod.pin(alloc, cachepath), mod.main }); + + if (!mod.has_no_zig_deps()) { + try w.writeAll(" .deps = &[_]*Package{"); + for (mod.deps) |moddep, j| { + if (moddep.main.len == 0) continue; + try w.print(" &_{s}", .{moddep.id[0..12]}); + if (j != mod.deps.len - 1) try w.writeAll(","); + } + try w.writeAll(" },\n"); + } + } + if (mod.c_include_dirs.len > 0) { + try w.writeAll(" .c_include_dirs = &.{"); + for (mod.c_include_dirs) |item, j| { + try w.print(" \"{}\"", .{std.zig.fmtEscapes(item)}); + if (j != mod.c_include_dirs.len - 1) try w.writeAll(","); + } + try w.writeAll(" },\n"); + } + if (mod.c_source_files.len > 0) { + try w.writeAll(" .c_source_files = &.{"); + for (mod.c_source_files) |item, j| { + try w.print(" \"{}\"", .{std.zig.fmtEscapes(item)}); + if (j != mod.c_source_files.len - 1) try w.writeAll(","); + } + try w.writeAll(" },\n"); + } + if (mod.c_source_flags.len > 0) { + try w.writeAll(" .c_source_flags = &.{"); + for (mod.c_source_flags) |item, j| { + try w.print(" \"{}\"", .{std.zig.fmtEscapes(item)}); + if (j != mod.c_source_flags.len - 1) try w.writeAll(","); + } + try w.writeAll(" },\n"); + } + if (mod.has_syslib_deps()) { + try w.writeAll(" .system_libs = &.{"); + for (mod.deps) |item, j| { + if (!(item.type == .system_lib)) continue; + try w.print(" \"{}\"", .{std.zig.fmtEscapes(item.name)}); + if (j != mod.deps.len - 1) try w.writeAll(","); + } + try w.writeAll(" },\n"); + } + if (mod.has_framework_deps()) { + try w.writeAll(" .frameworks = &.{"); + for (mod.deps) |item, j| { + if (!(item.type == .system_lib)) continue; + try w.print(" \"{}\"", .{std.zig.fmtEscapes(item.name)}); + if (j != mod.deps.len - 1) try w.writeAll(","); + } + try w.writeAll(" },\n"); + } + if (mod.vcpkg) { + try w.writeAll(" .vcpkg = true,\n"); + } + try w.writeAll(" };\n"); + + try done.append(mod); + _ = notdone.orderedRemove(i); + break; + } + } + if (notdone.items.len == len) { + u.fail("notdone still has {d} items", .{len}); + } + len = notdone.items.len; + } +} + +/// returns if all of the zig modules in needles are in haystack +fn contains_all(needles: []zigmod.Module, haystack: []const zigmod.Module) bool { + for (needles) |item| { + if (item.main.len > 0 and !u.list_contains_gen(zigmod.Module, haystack, item)) { + return false; + } + } + return true; +} + +fn print_pkgs(alloc: std.mem.Allocator, w: std.fs.File.Writer, m: zigmod.Module) !void { + try w.writeAll("struct {\n"); + for (m.deps) |d| { + if (d.main.len == 0) { + continue; + } + if (d.for_build) { + continue; + } + const ident = try zig_name_from_pkg_name(alloc, d.name); + try w.print(" pub const {s} = &package_data._{s};\n", .{ ident, d.id[0..12] }); + } + try w.writeAll("}"); +} + +fn print_imports(alloc: std.mem.Allocator, w: std.fs.File.Writer, m: zigmod.Module, path: string) !void { + for (m.deps) |d| { + if (d.main.len == 0) { + continue; + } + if (!d.for_build) { + continue; + } + const ident = try zig_name_from_pkg_name(alloc, d.name); + try w.print(" pub const {s} = @import(\"{}/{}/{s}\");\n", .{ ident, std.zig.fmtEscapes(path), std.zig.fmtEscapes(d.clean_path), d.main }); + } +} + +fn zig_name_from_pkg_name(alloc: std.mem.Allocator, name: string) !string { + var legal = name; + legal = try std.mem.replaceOwned(u8, alloc, legal, "-", "_"); + legal = try std.mem.replaceOwned(u8, alloc, legal, "/", "_"); + legal = try std.mem.replaceOwned(u8, alloc, legal, ".", "_"); + return legal; +} diff --git a/src/lib.zig b/src/lib.zig index 524d68c82d31b697dab362925e50b02ba4994059..ce4337acbd5c58aaee1a2d4cf130dae6b73c8261 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -13,6 +13,7 @@ pub const commands = struct { pub const zpm = @import("./cmd/zpm.zig"); pub const license = @import("./cmd/license.zig"); pub const aq = @import("./cmd/aq.zig"); + pub const generate = @import("./cmd/generate.zig"); }; pub fn init() !void { diff --git a/src/util/module.zig b/src/util/module.zig index 42048948d1abd5479c2edd72185ed548965527d0..e4736183f27fb3e9ad978288246a0bdc14aa9810 100644 --- a/src/util/module.zig +++ b/src/util/module.zig @@ -157,4 +157,24 @@ pub const Module = struct { } return false; } + + pub fn pin(self: Module, alloc: std.mem.Allocator, cachepath: string) !string { + return switch (self.type) { + .local => "", + .system_lib => "", + .framework => "", + else => |sub| { + var cdir = try std.fs.cwd().openDir(cachepath, .{}); + defer cdir.close(); + var mdir = try cdir.openDir(self.clean_path, .{}); + defer mdir.close(); + return switch (sub) { + .local, .system_lib, .framework => unreachable, + .git => try u.git_rev_HEAD(alloc, mdir), + .hg => @panic("TODO"), + .http => @panic("TODO"), + }; + }, + }; + } }; -- 2.54.0