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}