aboutsummaryrefslogtreecommitdiff
path: root/lib/std/Build/WriteFileStep.zig
blob: 3a30aba1905354fe3d2ec0537d58ded17f8b5d34 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
//! WriteFileStep is primarily used to create a directory in an appropriate
//! location inside the local cache which has a set of files that have either
//! been generated during the build, or are copied from the source package.
//!
//! However, this step has an additional capability of writing data to paths
//! relative to the package root, effectively mutating the package's source
//! files. Be careful with the latter functionality; it should not be used
//! during the normal build process, but as a utility run by a developer with
//! intention to update source files, which will then be committed to version
//! control.

step: Step,
builder: *std.Build,
/// The elements here are pointers because we need stable pointers for the
/// GeneratedFile field.
files: std.ArrayListUnmanaged(*File),
output_source_files: std.ArrayListUnmanaged(OutputSourceFile),

pub const base_id = .write_file;

pub const File = struct {
    generated_file: std.Build.GeneratedFile,
    sub_path: []const u8,
    contents: Contents,
};

pub const OutputSourceFile = struct {
    contents: Contents,
    sub_path: []const u8,
};

pub const Contents = union(enum) {
    bytes: []const u8,
    copy: std.Build.FileSource,
};

pub fn init(builder: *std.Build) WriteFileStep {
    return .{
        .builder = builder,
        .step = Step.init(.write_file, "writefile", builder.allocator, make),
        .files = .{},
        .output_source_files = .{},
    };
}

pub fn add(wf: *WriteFileStep, sub_path: []const u8, bytes: []const u8) void {
    const gpa = wf.builder.allocator;
    const file = gpa.create(File) catch @panic("OOM");
    file.* = .{
        .generated_file = .{ .step = &wf.step },
        .sub_path = wf.builder.dupePath(sub_path),
        .contents = .{ .bytes = wf.builder.dupe(bytes) },
    };
    wf.files.append(gpa, file) catch @panic("OOM");
}

/// Place the file into the generated directory within the local cache,
/// along with all the rest of the files added to this step. The parameter
/// here is the destination path relative to the local cache directory
/// associated with this WriteFileStep. It may be a basename, or it may
/// include sub-directories, in which case this step will ensure the
/// required sub-path exists.
/// This is the option expected to be used most commonly with `addCopyFile`.
pub fn addCopyFile(wf: *WriteFileStep, source: std.Build.FileSource, sub_path: []const u8) void {
    const gpa = wf.builder.allocator;
    const file = gpa.create(File) catch @panic("OOM");
    file.* = .{
        .generated_file = .{ .step = &wf.step },
        .sub_path = wf.builder.dupePath(sub_path),
        .contents = .{ .copy = source },
    };
    wf.files.append(gpa, file) catch @panic("OOM");
}

/// A path relative to the package root.
/// Be careful with this because it updates source files. This should not be
/// used as part of the normal build process, but as a utility occasionally
/// run by a developer with intent to modify source files and then commit
/// those changes to version control.
/// A file added this way is not available with `getFileSource`.
pub fn addCopyFileToSource(wf: *WriteFileStep, source: std.Build.FileSource, sub_path: []const u8) void {
    wf.output_source_files.append(wf.builder.allocator, .{
        .contents = .{ .copy = source },
        .sub_path = sub_path,
    }) catch @panic("OOM");
}

/// Gets a file source for the given sub_path. If the file does not exist, returns `null`.
pub fn getFileSource(wf: *WriteFileStep, sub_path: []const u8) ?std.Build.FileSource {
    for (wf.files.items) |file| {
        if (std.mem.eql(u8, file.sub_path, sub_path)) {
            return .{ .generated = &file.generated_file };
        }
    }
    return null;
}

fn make(step: *Step) !void {
    const wf = @fieldParentPtr(WriteFileStep, "step", step);

    // Writing to source files is kind of an extra capability of this
    // WriteFileStep - arguably it should be a different step. But anyway here
    // it is, it happens unconditionally and does not interact with the other
    // files here.
    for (wf.output_source_files.items) |output_source_file| {
        const basename = fs.path.basename(output_source_file.sub_path);
        if (fs.path.dirname(output_source_file.sub_path)) |dirname| {
            var dir = try wf.builder.build_root.handle.makeOpenPath(dirname, .{});
            defer dir.close();
            try writeFile(wf, dir, output_source_file.contents, basename);
        } else {
            try writeFile(wf, wf.builder.build_root.handle, output_source_file.contents, basename);
        }
    }

    // The cache is used here not really as a way to speed things up - because writing
    // the data to a file would probably be very fast - but as a way to find a canonical
    // location to put build artifacts.

    // If, for example, a hard-coded path was used as the location to put WriteFileStep
    // files, then two WriteFileSteps executing in parallel might clobber each other.

    var man = wf.builder.cache.obtain();
    defer man.deinit();

    // Random bytes to make WriteFileStep unique. Refresh this with
    // new random bytes when WriteFileStep implementation is modified
    // in a non-backwards-compatible way.
    man.hash.add(@as(u32, 0xd767ee59));

    for (wf.files.items) |file| {
        man.hash.addBytes(file.sub_path);
        switch (file.contents) {
            .bytes => |bytes| {
                man.hash.addBytes(bytes);
            },
            .copy => |file_source| {
                _ = try man.addFile(file_source.getPath(wf.builder), null);
            },
        }
    }

    if (man.hit() catch |err| failWithCacheError(man, err)) {
        // Cache hit, skip writing file data.
        const digest = man.final();
        for (wf.files.items) |file| {
            file.generated_file.path = try wf.builder.cache_root.join(
                wf.builder.allocator,
                &.{ "o", &digest, file.sub_path },
            );
        }
        return;
    }

    const digest = man.final();
    const cache_path = "o" ++ fs.path.sep_str ++ digest;

    var cache_dir = wf.builder.cache_root.handle.makeOpenPath(cache_path, .{}) catch |err| {
        std.debug.print("unable to make path {s}: {s}\n", .{ cache_path, @errorName(err) });
        return err;
    };
    defer cache_dir.close();

    for (wf.files.items) |file| {
        const basename = fs.path.basename(file.sub_path);
        if (fs.path.dirname(file.sub_path)) |dirname| {
            var dir = try wf.builder.cache_root.handle.makeOpenPath(dirname, .{});
            defer dir.close();
            try writeFile(wf, dir, file.contents, basename);
        } else {
            try writeFile(wf, cache_dir, file.contents, basename);
        }

        file.generated_file.path = try wf.builder.cache_root.join(
            wf.builder.allocator,
            &.{ cache_path, file.sub_path },
        );
    }

    try man.writeManifest();
}

fn writeFile(wf: *WriteFileStep, dir: fs.Dir, contents: Contents, basename: []const u8) !void {
    // TODO after landing concurrency PR, improve error reporting here
    switch (contents) {
        .bytes => |bytes| return dir.writeFile(basename, bytes),
        .copy => |file_source| {
            const source_path = file_source.getPath(wf.builder);
            const prev_status = try fs.Dir.updateFile(fs.cwd(), source_path, dir, basename, .{});
            _ = prev_status; // TODO logging (affected by open PR regarding concurrency)
        },
    }
}

/// TODO consolidate this with the same function in RunStep?
/// Also properly deal with concurrency (see open PR)
fn failWithCacheError(man: std.Build.Cache.Manifest, err: anyerror) noreturn {
    const i = man.failed_file_index orelse failWithSimpleError(err);
    const pp = man.files.items[i].prefixed_path orelse failWithSimpleError(err);
    const prefix = man.cache.prefixes()[pp.prefix].path orelse "";
    std.debug.print("{s}: {s}/{s}\n", .{ @errorName(err), prefix, pp.sub_path });
    std.process.exit(1);
}

fn failWithSimpleError(err: anyerror) noreturn {
    std.debug.print("{s}\n", .{@errorName(err)});
    std.process.exit(1);
}

const std = @import("../std.zig");
const Step = std.Build.Step;
const fs = std.fs;
const ArrayList = std.ArrayList;

const WriteFileStep = @This();