master
1const std = @import("std");
2const mem = std.mem;
3const fs = std.fs;
4const Step = std.Build.Step;
5const LazyPath = std.Build.LazyPath;
6const InstallDir = @This();
7
8step: Step,
9options: Options,
10
11pub const base_id: Step.Id = .install_dir;
12
13pub const Options = struct {
14 source_dir: LazyPath,
15 install_dir: std.Build.InstallDir,
16 install_subdir: []const u8,
17 /// File paths which end in any of these suffixes will be excluded
18 /// from being installed.
19 exclude_extensions: []const []const u8 = &.{},
20 /// Only file paths which end in any of these suffixes will be included
21 /// in installation. `null` means all suffixes are valid for this option.
22 /// `exclude_extensions` take precedence over `include_extensions`
23 include_extensions: ?[]const []const u8 = null,
24 /// File paths which end in any of these suffixes will result in
25 /// empty files being installed. This is mainly intended for large
26 /// test.zig files in order to prevent needless installation bloat.
27 /// However if the files were not present at all, then
28 /// `@import("test.zig")` would be a compile error.
29 blank_extensions: []const []const u8 = &.{},
30
31 fn dupe(opts: Options, b: *std.Build) Options {
32 return .{
33 .source_dir = opts.source_dir.dupe(b),
34 .install_dir = opts.install_dir.dupe(b),
35 .install_subdir = b.dupe(opts.install_subdir),
36 .exclude_extensions = b.dupeStrings(opts.exclude_extensions),
37 .include_extensions = if (opts.include_extensions) |incs| b.dupeStrings(incs) else null,
38 .blank_extensions = b.dupeStrings(opts.blank_extensions),
39 };
40 }
41};
42
43pub fn create(owner: *std.Build, options: Options) *InstallDir {
44 const install_dir = owner.allocator.create(InstallDir) catch @panic("OOM");
45 install_dir.* = .{
46 .step = Step.init(.{
47 .id = base_id,
48 .name = owner.fmt("install {s}/", .{options.source_dir.getDisplayName()}),
49 .owner = owner,
50 .makeFn = make,
51 }),
52 .options = options.dupe(owner),
53 };
54 options.source_dir.addStepDependencies(&install_dir.step);
55 return install_dir;
56}
57
58fn make(step: *Step, options: Step.MakeOptions) !void {
59 _ = options;
60 const b = step.owner;
61 const install_dir: *InstallDir = @fieldParentPtr("step", step);
62 step.clearWatchInputs();
63 const arena = b.allocator;
64 const dest_prefix = b.getInstallPath(install_dir.options.install_dir, install_dir.options.install_subdir);
65 const src_dir_path = install_dir.options.source_dir.getPath3(b, step);
66 const need_derived_inputs = try step.addDirectoryWatchInput(install_dir.options.source_dir);
67 var src_dir = src_dir_path.root_dir.handle.openDir(src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
68 return step.fail("unable to open source directory '{f}': {s}", .{
69 src_dir_path, @errorName(err),
70 });
71 };
72 defer src_dir.close();
73 var it = try src_dir.walk(arena);
74 var all_cached = true;
75 next_entry: while (try it.next()) |entry| {
76 for (install_dir.options.exclude_extensions) |ext| {
77 if (mem.endsWith(u8, entry.path, ext)) continue :next_entry;
78 }
79 if (install_dir.options.include_extensions) |incs| {
80 for (incs) |inc| {
81 if (mem.endsWith(u8, entry.path, inc)) break;
82 } else {
83 continue :next_entry;
84 }
85 }
86
87 const src_path = try install_dir.options.source_dir.join(b.allocator, entry.path);
88 const dest_path = b.pathJoin(&.{ dest_prefix, entry.path });
89 switch (entry.kind) {
90 .directory => {
91 if (need_derived_inputs) _ = try step.addDirectoryWatchInput(src_path);
92 const p = try step.installDir(dest_path);
93 all_cached = all_cached and p == .existed;
94 },
95 .file => {
96 for (install_dir.options.blank_extensions) |ext| {
97 if (mem.endsWith(u8, entry.path, ext)) {
98 try b.truncateFile(dest_path);
99 continue :next_entry;
100 }
101 }
102
103 const p = try step.installFile(src_path, dest_path);
104 all_cached = all_cached and p == .fresh;
105 },
106 else => continue,
107 }
108 }
109
110 step.result_cached = all_cached;
111}