Skip to content

Commit 12420b2

Browse files
committed
Added cache logic
1 parent ea336d3 commit 12420b2

File tree

1 file changed

+384
-0
lines changed

1 file changed

+384
-0
lines changed

src/cache.zig

+384
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
// Generally Questions
2+
// How will the compiler/roc symlink be set?
3+
// Spec here https://github.com/roc-lang/roc/issues/7517
4+
const builtin = @import("builtin");
15
const std = @import("std");
6+
const testing = std.testing;
7+
const Allocator = std.mem.Allocator;
28
const base = @import("base.zig");
39
const canonicalize = @import("check/canonicalize.zig");
410

@@ -19,3 +25,381 @@ pub fn getPackageRootAbsDir(url_data: Package.Url, gpa: std.mem.Allocator) []con
1925

2026
@panic("not implemented");
2127
}
28+
29+
pub const CacheFileQuery = union(enum) {
30+
compiler: Compiler,
31+
build,
32+
packages_src,
33+
packages_build,
34+
35+
pub const Compiler = struct {
36+
version_name: []const u8,
37+
};
38+
};
39+
40+
pub const CacheFileData = union(enum) {
41+
compiler: Compiler,
42+
build,
43+
packages_src,
44+
packages_build,
45+
46+
pub const Compiler = struct {
47+
version_name: []const u8,
48+
binary: []const u8,
49+
};
50+
};
51+
52+
const target_os = builtin.target.os.tag;
53+
fn setEnvVar(allocator: Allocator, name: []const u8, value: []const u8) !void {
54+
// SKILL_ISSUE: it's required to use -lc to run the tests because of the
55+
// c library calls. Is there a better way to run a single test file?
56+
// zig test ./src/cache.zig -lc
57+
const c_str = try allocator.dupeZ(u8, value);
58+
defer allocator.free(c_str);
59+
60+
return {
61+
switch (target_os) {
62+
.macos, .linux => {
63+
const c = @cImport({
64+
@cInclude("stdlib.h");
65+
});
66+
if (c.setenv(name.ptr, c_str.ptr, 1) != 0) {
67+
return error.SetEnvFailed;
68+
}
69+
},
70+
//.windows => {
71+
// TODO: figure out windows implementation
72+
//const c = @cImport({
73+
//@cInclude("windows.h");
74+
//});
75+
//if (c.SetEnvironmentVariableA(name.ptr, value.ptr) == 0) {
76+
//return error.SetEnvFailed;
77+
//}
78+
//},
79+
else => {
80+
@compileError("\"" ++ @tagName(target_os) ++ "\" is not supported in cache.zig");
81+
},
82+
}
83+
};
84+
}
85+
86+
pub fn unsetEnvVar(name: []const u8) !void {
87+
// SKILL_ISSUE: it's required to use -lc to run the tests because of the
88+
// c library calls. Is there a better way to run a single test file?
89+
// zig test ./src/cache.zig -lc
90+
return {
91+
switch (target_os) {
92+
.macos, .linux => {
93+
const c = @cImport({
94+
@cInclude("stdlib.h");
95+
});
96+
if (c.unsetenv(name.ptr) != 0) {
97+
return error.UnsetEnvFailed;
98+
}
99+
},
100+
// FIX_FOR_WINDOWS:
101+
//.windows => {
102+
// TODO: figure out windows implementation
103+
//const c = @cImport({
104+
//@cInclude("windows.h");
105+
//});
106+
//if (c.SetEnvironmentVariableA(name.ptr, value.ptr) == 0) {
107+
//return error.SetEnvFailed;
108+
//}
109+
//},
110+
else => {
111+
@compileError("\"" ++ @tagName(target_os) ++ "\" is not supported in cache.zig");
112+
},
113+
}
114+
};
115+
}
116+
117+
fn getDefaultCacheFolder(allocator: Allocator) ![]u8 {
118+
return {
119+
switch (target_os) {
120+
.macos, .linux => {
121+
const home_dir = try std.process.getEnvVarOwned(allocator, "HOME");
122+
defer allocator.free(home_dir);
123+
const default_cache_folder_parts = [_][]const u8{
124+
home_dir,
125+
".cache",
126+
};
127+
const default_cache_folder = try std.fs.path.join(allocator, &default_cache_folder_parts);
128+
return default_cache_folder;
129+
},
130+
.windows => {
131+
const default_cache_folder_parts = [_][]const u8{"%APPDATA%"};
132+
const default_cache_folder = try std.fs.path.join(allocator, &default_cache_folder_parts);
133+
return default_cache_folder;
134+
},
135+
else => {
136+
@compileError("\"" ++ @tagName(target_os) ++ "\" is not supported in cache.zig");
137+
},
138+
}
139+
};
140+
}
141+
142+
const xdg_cache_home_var_name = "XDG_CACHE_HOME";
143+
fn getBaseRocCacheFolder(allocator: Allocator, sub_path: []const u8) ![]u8 {
144+
const xdg_cache_home = std.process.getEnvVarOwned(allocator, xdg_cache_home_var_name) catch null;
145+
defer {
146+
if (xdg_cache_home) |x| {
147+
allocator.free(x);
148+
}
149+
}
150+
const base_dir = if (xdg_cache_home) |x|
151+
try allocator.dupe(u8, x)
152+
else
153+
try getDefaultCacheFolder(allocator);
154+
155+
defer allocator.free(base_dir);
156+
const roc_cache_parts = [_][]const u8{
157+
base_dir,
158+
"roc",
159+
sub_path,
160+
};
161+
return try std.fs.path.join(allocator, &roc_cache_parts);
162+
}
163+
164+
fn getCompilerPath(allocator: Allocator, version_name: []const u8) ![]u8 {
165+
const sub_path_parts = [_][]const u8{ "compiler", version_name };
166+
const sub_path = try std.fs.path.join(allocator, &sub_path_parts);
167+
defer allocator.free(sub_path);
168+
return try getBaseRocCacheFolder(allocator, sub_path);
169+
}
170+
171+
const SaveCacheFileError = error{
172+
FailedToMakeCacheFolder,
173+
FailedToMakeCacheFile,
174+
FailedToWriteToCacheFile,
175+
};
176+
177+
fn saveCacheFile(file_cache_path: []const u8, file_binary: []const u8) SaveCacheFileError!void {
178+
const cwd = std.fs.cwd();
179+
const roc_cache_folder = std.fs.path.dirname(file_cache_path);
180+
if (roc_cache_folder) |x| {
181+
cwd.makePath(x) catch {
182+
return error.FailedToMakeCacheFolder;
183+
};
184+
}
185+
const file = cwd.createFile(file_cache_path, .{}) catch {
186+
return SaveCacheFileError.FailedToMakeCacheFile;
187+
};
188+
file.writeAll(file_binary) catch {
189+
return SaveCacheFileError.FailedToWriteToCacheFile;
190+
};
191+
}
192+
193+
fn saveToCacheInternal(
194+
// SKILL_ISSUE: force my formatter to put the arguments in a vertical line
195+
allocator: Allocator,
196+
roc_cache_file: CacheFileData,
197+
comptime save: fn (file_cache_path: []const u8, file_binary: []const u8) SaveCacheFileError!void,
198+
) !void {
199+
const file_cache_path = switch (roc_cache_file) {
200+
.compiler => |value| try getCompilerPath(allocator, value.version_name),
201+
else => @panic("FIX ME!!!!!!"),
202+
};
203+
204+
defer allocator.free(file_cache_path);
205+
206+
const file_binary = switch (roc_cache_file) {
207+
.compiler => |value| value.binary,
208+
else => @panic("FIX ME!!!!!!"),
209+
};
210+
211+
try save(file_cache_path, file_binary);
212+
}
213+
214+
pub fn saveToCache(allocator: Allocator, roc_cache_file: CacheFileData) !void {
215+
try saveToCacheInternal(allocator, roc_cache_file, saveCacheFile);
216+
}
217+
218+
const GetCacheFileError = error{
219+
AccessDeniedToCache,
220+
Unexpected,
221+
OutOfMemory,
222+
};
223+
224+
fn getCacheFile(allocator: Allocator, file_cache_path: []const u8) GetCacheFileError!?[]const u8 {
225+
const File = std.fs.File;
226+
const OpenError = File.OpenError;
227+
const file = std.fs.cwd().openFile(file_cache_path, .{ .mode = .read_only }) catch |e| {
228+
return switch (e) {
229+
OpenError.FileNotFound => null,
230+
OpenError.AccessDenied => GetCacheFileError.AccessDeniedToCache,
231+
OpenError.NameTooLong => null,
232+
else => GetCacheFileError.Unexpected,
233+
};
234+
};
235+
defer file.close();
236+
237+
const GetSeekPosError = File.GetSeekPosError;
238+
const file_size = file.getEndPos() catch |e| {
239+
return switch (e) {
240+
GetSeekPosError.AccessDenied => GetCacheFileError.AccessDeniedToCache,
241+
else => GetCacheFileError.Unexpected,
242+
};
243+
};
244+
const buf = try allocator.alloc(u8, file_size);
245+
246+
const ReadError = File.ReadError;
247+
_ = file.readAll(buf) catch |e| {
248+
return switch (e) {
249+
ReadError.AccessDenied => GetCacheFileError.AccessDeniedToCache,
250+
else => GetCacheFileError.Unexpected,
251+
};
252+
};
253+
return buf;
254+
}
255+
256+
fn tryToGetCacheFileInternal(allocator: Allocator, cache_file_query: CacheFileQuery) !?[]const u8 {
257+
const file_cache_path = switch (cache_file_query) {
258+
.compiler => |value| try getCompilerPath(allocator, value.version_name),
259+
else => @panic("FIX ME!!!!!!"),
260+
};
261+
defer allocator.free(file_cache_path);
262+
return try getCacheFile(allocator, file_cache_path);
263+
}
264+
265+
pub fn tryToGetCacheFile(allocator: Allocator, cache_file_query: CacheFileQuery) !?[]const u8 {
266+
return try tryToGetCacheFileInternal(allocator, cache_file_query);
267+
}
268+
269+
fn create_test_cache_folder(allocator: Allocator) !testing.TmpDir {
270+
const tmp_dir = testing.tmpDir(.{});
271+
const path = try tmp_dir.dir.realpathAlloc(allocator, ".");
272+
defer allocator.free(path);
273+
try setEnvVar(allocator, xdg_cache_home_var_name, path);
274+
return tmp_dir;
275+
}
276+
277+
pub fn create_save_stub(allocator: Allocator) type {
278+
return struct {
279+
const Self = @This();
280+
var file_cache_path_list = std.ArrayList([]const u8).init(allocator);
281+
var file_binary_list = std.ArrayList([]const u8).init(allocator);
282+
283+
fn deinit() void {
284+
for (Self.file_cache_path_list.items) |path| {
285+
allocator.free(path);
286+
}
287+
for (Self.file_binary_list.items) |binary| {
288+
allocator.free(binary);
289+
}
290+
291+
Self.file_cache_path_list.deinit();
292+
Self.file_binary_list.deinit();
293+
}
294+
295+
fn closure(file_cache_path: []const u8, file_binary: []const u8) SaveCacheFileError!void {
296+
const file_cache_path_copy = allocator.dupe(u8, file_cache_path) catch {
297+
@panic("Failed to copy cache path string in test");
298+
};
299+
Self.file_cache_path_list.append(file_cache_path_copy) catch {
300+
@panic("Failed to append to file_cache_path_list in test");
301+
};
302+
const file_binary_copy = allocator.dupe(u8, file_binary) catch {
303+
@panic("Failed to copy cache path string in test");
304+
};
305+
Self.file_binary_list.append(file_binary_copy) catch {
306+
@panic("Failed to append to file_binary_list in test");
307+
};
308+
}
309+
310+
fn assert_save_called_with_correct_arguments(file_cache_path: []const u8, file_binary: []const u8) !void {
311+
try testing.expectEqual(1, Self.file_cache_path_list.items.len);
312+
try testing.expectEqual(1, Self.file_binary_list.items.len);
313+
try testing.expectEqualStrings(file_cache_path, Self.file_cache_path_list.items[0]);
314+
try testing.expectEqualStrings(file_binary, Self.file_binary_list.items[0]);
315+
}
316+
};
317+
}
318+
319+
fn get_home_directory(allocator: Allocator) ![]u8 {
320+
return {
321+
switch (target_os) {
322+
.macos, .linux => {
323+
return try std.process.getEnvVarOwned(allocator, "HOME");
324+
},
325+
.windows => {
326+
return "%APPDATA%";
327+
},
328+
else => {
329+
@compileError("\"" ++ @tagName(target_os) ++ "\" is not supported in cache.zig");
330+
},
331+
}
332+
};
333+
}
334+
335+
test "Should save to XDG_CACHE_HOME if it is set for compiler cache" {
336+
const allocator = testing.allocator;
337+
var tmp_dir = try create_test_cache_folder(allocator);
338+
defer tmp_dir.cleanup();
339+
defer {
340+
unsetEnvVar(xdg_cache_home_var_name) catch {
341+
@panic("failed to unset " ++ xdg_cache_home_var_name);
342+
};
343+
}
344+
const cache_file = CacheFileData.Compiler{
345+
.version_name = "1.0.0",
346+
.binary = "A roc compiler binary",
347+
};
348+
try saveToCache(allocator, CacheFileData{ .compiler = cache_file });
349+
350+
const binary = try tryToGetCacheFile(allocator, CacheFileQuery{ .compiler = CacheFileQuery.Compiler{
351+
.version_name = cache_file.version_name,
352+
} }) orelse @panic("Binary file should exist");
353+
defer allocator.free(binary);
354+
355+
try testing.expectEqual(cache_file.binary.len, binary.len);
356+
try testing.expectEqualStrings(cache_file.binary, binary);
357+
}
358+
359+
test "Should save to .cache folder if XDG_CACHE_HOME is not set for compiler cache" {
360+
const allocator = testing.allocator;
361+
const home_dir = try get_home_directory(allocator);
362+
defer allocator.free(home_dir);
363+
364+
const cache_file = CacheFileData.Compiler{
365+
.version_name = "1.0.0",
366+
.binary = "A roc compiler binary",
367+
};
368+
369+
const save_stub_context = create_save_stub(allocator);
370+
defer save_stub_context.deinit();
371+
372+
try saveToCacheInternal(allocator, CacheFileData{ .compiler = cache_file }, save_stub_context.closure);
373+
374+
const expected_binary = cache_file.binary;
375+
const expected_file_cache_path = try std.fs.path.join(allocator, &[_][]const u8{
376+
home_dir,
377+
".cache",
378+
"roc",
379+
"compiler",
380+
cache_file.version_name,
381+
});
382+
defer allocator.free(expected_file_cache_path);
383+
try save_stub_context.assert_save_called_with_correct_arguments(expected_file_cache_path, expected_binary);
384+
}
385+
386+
test "Should return if there is no cache file" {
387+
const allocator = testing.allocator;
388+
const home_dir = try get_home_directory(allocator);
389+
defer allocator.free(home_dir);
390+
const fake_path = try std.fs.path.join(allocator, &[_][]const u8{
391+
home_dir,
392+
"fake-cache",
393+
});
394+
defer allocator.free(fake_path);
395+
try setEnvVar(allocator, xdg_cache_home_var_name, fake_path);
396+
const cache_file_query = CacheFileQuery.Compiler{
397+
.version_name = "1.0.0",
398+
};
399+
const binary = try tryToGetCacheFile(allocator, CacheFileQuery{ .compiler = cache_file_query });
400+
if (binary) |b| {
401+
defer allocator.free(b);
402+
}
403+
404+
try testing.expectEqual(null, binary);
405+
}

0 commit comments

Comments
 (0)