authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-01-10 00:38:36-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-01-11 15:39:49-08:00
loge0401498e928a539677f0f9eed843a0453bc8c33
tree3ce6ead2fddf85de3df29723929c497b91a56bc4
parentd4e829d0a0f260e4dacbe293cd282af8ba2e4a30

package manager: compute hash, move tmp dir into global cache


1 files changed, 195 insertions(+), 49 deletions(-)

src/Package.zig+195-49
......@@ -10,6 +10,7 @@ const Hash = std.crypto.hash.sha2.Sha256;
1010const Compilation = @import("Compilation.zig");
1111const Module = @import("Module.zig");
1212const ThreadPool = @import("ThreadPool.zig");
13const WaitGroup = @import("WaitGroup.zig");
1314
1415pub const Table = std.StringHashMapUnmanaged(*Package);
1516
......@@ -201,7 +202,13 @@ pub fn fetchAndAddDependencies(
201202 continue;
202203 };
203204
204 const sub_pkg = try fetchAndUnpack(http_client, global_cache_directory, url, expected_hash);
205 const sub_pkg = try fetchAndUnpack(
206 thread_pool,
207 http_client,
208 global_cache_directory,
209 url,
210 expected_hash,
211 );
205212
206213 try sub_pkg.fetchAndAddDependencies(
207214 thread_pool,
......@@ -218,6 +225,7 @@ pub fn fetchAndAddDependencies(
218225}
219226
220227fn fetchAndUnpack(
228 thread_pool: *ThreadPool,
221229 http_client: *std.http.Client,
222230 global_cache_directory: Compilation.Directory,
223231 url: []const u8,
......@@ -225,71 +233,99 @@ fn fetchAndUnpack(
225233) !*Package {
226234 const gpa = http_client.allocator;
227235
228 // TODO check if the expected_hash is already present in the global package cache, and
229 // thereby avoid both fetching and unpacking.
236 // Check if the expected_hash is already present in the global package
237 // cache, and thereby avoid both fetching and unpacking.
238 const s = fs.path.sep_str;
239 if (expected_hash) |h| {
240 const pkg_dir_sub_path = "p" ++ s ++ hexDigest(h);
241 _ = pkg_dir_sub_path;
242 @panic("TODO check the p dir for the package");
243 }
230244
231245 const uri = try std.Uri.parse(url);
232246
233 var tmp_directory: Compilation.Directory = d: {
234 const s = fs.path.sep_str;
235 const rand_int = std.crypto.random.int(u64);
247 const rand_int = std.crypto.random.int(u64);
248 const tmp_dir_sub_path = "tmp" ++ s ++ hex64(rand_int);
236249
237 const tmp_dir_sub_path = try std.fmt.allocPrint(gpa, "tmp" ++ s ++ "{x}", .{rand_int});
250 const actual_hash = a: {
251 var tmp_directory: Compilation.Directory = d: {
252 const path = try global_cache_directory.join(gpa, &.{tmp_dir_sub_path});
253 errdefer gpa.free(path);
238254
239 const path = try global_cache_directory.join(gpa, &.{tmp_dir_sub_path});
240 errdefer gpa.free(path);
255 const iterable_dir = try global_cache_directory.handle.makeOpenPathIterable(tmp_dir_sub_path, .{});
256 errdefer iterable_dir.close();
241257
242 const handle = try global_cache_directory.handle.makeOpenPath(tmp_dir_sub_path, .{});
243 errdefer handle.close();
244
245 break :d .{
246 .path = path,
247 .handle = handle,
258 break :d .{
259 .path = path,
260 .handle = iterable_dir.dir,
261 };
248262 };
249 };
250 defer tmp_directory.closeAndFree(gpa);
263 defer tmp_directory.closeAndFree(gpa);
251264
252 var req = try http_client.request(uri, .{}, .{});
253 defer req.deinit();
265 var req = try http_client.request(uri, .{}, .{});
266 defer req.deinit();
254267
255 if (mem.endsWith(u8, uri.path, ".tar.gz")) {
256 // I observed the gzip stream to read 1 byte at a time, so I am using a
257 // buffered reader on the front of it.
258 var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, req.reader());
268 if (mem.endsWith(u8, uri.path, ".tar.gz")) {
269 // I observed the gzip stream to read 1 byte at a time, so I am using a
270 // buffered reader on the front of it.
271 var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, req.reader());
259272
260 var gzip_stream = try std.compress.gzip.gzipStream(gpa, br.reader());
261 defer gzip_stream.deinit();
273 var gzip_stream = try std.compress.gzip.gzipStream(gpa, br.reader());
274 defer gzip_stream.deinit();
262275
263 try std.tar.pipeToFileSystem(tmp_directory.handle, gzip_stream.reader(), .{});
264 } else {
265 // TODO: show the build.zig.ini file and line number
266 std.log.err("{s}: unknown package extension for path '{s}'", .{ url, uri.path });
267 return error.UnknownPackageExtension;
268 }
276 try std.tar.pipeToFileSystem(tmp_directory.handle, gzip_stream.reader(), .{
277 .strip_components = 1,
278 });
279 } else {
280 // TODO: show the build.zig.ini file and line number
281 std.log.err("{s}: unknown package extension for path '{s}'", .{ url, uri.path });
282 return error.UnknownPackageExtension;
283 }
269284
270 // TODO: delete files not included in the package prior to computing the package hash.
271 // for example, if the ini file has directives to include/not include certain files,
272 // apply those rules directly to the filesystem right here. This ensures that files
273 // not protected by the hash are not present on the file system.
285 // TODO: delete files not included in the package prior to computing the package hash.
286 // for example, if the ini file has directives to include/not include certain files,
287 // apply those rules directly to the filesystem right here. This ensures that files
288 // not protected by the hash are not present on the file system.
274289
275 const actual_hash = try computePackageHash(tmp_directory);
290 const actual_hash = try computePackageHash(thread_pool, .{ .dir = tmp_directory.handle });
276291
277 if (expected_hash) |h| {
278 if (!mem.eql(u8, &h, &actual_hash)) {
279 // TODO: show the build.zig.ini file and line number
280 std.log.err("{s}: hash mismatch: expected: {s}, actual: {s}", .{
281 url, h, actual_hash,
282 });
283 return error.PackageHashMismatch;
292 if (expected_hash) |h| {
293 if (!mem.eql(u8, &h, &actual_hash)) {
294 // TODO: show the build.zig.ini file and line number
295 std.log.err("{s}: hash mismatch: expected: {s}, actual: {s}", .{
296 url, h, actual_hash,
297 });
298 return error.PackageHashMismatch;
299 }
284300 }
285 }
286301
287 if (true) @panic("TODO move the tmp dir into place");
302 break :a actual_hash;
303 };
304
305 {
306 // Rename the temporary directory into the global package cache.
307 const pkg_dir_sub_path = "p" ++ s ++ hexDigest(actual_hash);
308 var handled_missing_dir = false;
309 while (true) {
310 global_cache_directory.handle.rename(tmp_dir_sub_path, pkg_dir_sub_path) catch |err| switch (err) {
311 error.FileNotFound => {
312 if (handled_missing_dir) return err;
313 global_cache_directory.handle.makeDir("p") catch |mkd_err| switch (mkd_err) {
314 error.PathAlreadyExists => handled_missing_dir = true,
315 else => |e| return e,
316 };
317 continue;
318 },
319 else => |e| return e,
320 };
321 break;
322 }
323 }
288324
289325 if (expected_hash == null) {
290326 // TODO: show the build.zig.ini file and line number
291327 std.log.err("{s}: missing hash:\nhash={s}", .{
292 url, actual_hash,
328 url, std.fmt.fmtSliceHexLower(&actual_hash),
293329 });
294330 return error.PackageDependencyMissingHash;
295331 }
......@@ -303,7 +339,117 @@ fn fetchAndUnpack(
303339 //root_src_path: []const u8,
304340}
305341
306fn computePackageHash(pkg_directory: Compilation.Directory) ![Hash.digest_length]u8 {
307 _ = pkg_directory;
308 @panic("TODO computePackageHash");
342const HashedFile = struct {
343 path: []const u8,
344 hash: [Hash.digest_length]u8,
345 failure: Error!void,
346
347 const Error = fs.File.OpenError || fs.File.ReadError;
348
349 fn lessThan(context: void, lhs: *const HashedFile, rhs: *const HashedFile) bool {
350 _ = context;
351 return mem.lessThan(u8, lhs.path, rhs.path);
352 }
353};
354
355fn computePackageHash(
356 thread_pool: *ThreadPool,
357 pkg_dir: fs.IterableDir,
358) ![Hash.digest_length]u8 {
359 const gpa = thread_pool.allocator;
360
361 // We'll use an arena allocator for the path name strings since they all
362 // need to be in memory for sorting.
363 var arena_instance = std.heap.ArenaAllocator.init(gpa);
364 defer arena_instance.deinit();
365 const arena = arena_instance.allocator();
366
367 // Collect all files, recursively, then sort.
368 var all_files = std.ArrayList(*HashedFile).init(gpa);
369 defer all_files.deinit();
370
371 var walker = try pkg_dir.walk(gpa);
372 defer walker.deinit();
373
374 {
375 // The final hash will be a hash of each file hashed independently. This
376 // allows hashing in parallel.
377 var wait_group: WaitGroup = .{};
378 defer wait_group.wait();
379
380 while (try walker.next()) |entry| {
381 switch (entry.kind) {
382 .Directory => continue,
383 .File => {},
384 else => return error.IllegalFileTypeInPackage,
385 }
386 const hashed_file = try arena.create(HashedFile);
387 hashed_file.* = .{
388 .path = try arena.dupe(u8, entry.path),
389 .hash = undefined, // to be populated by the worker
390 .failure = undefined, // to be populated by the worker
391 };
392
393 wait_group.start();
394 try thread_pool.spawn(workerHashFile, .{ pkg_dir.dir, hashed_file, &wait_group });
395 }
396 }
397
398 std.sort.sort(*HashedFile, all_files.items, {}, HashedFile.lessThan);
399
400 var hasher = Hash.init(.{});
401 var any_failures = false;
402 for (all_files.items) |hashed_file| {
403 hashed_file.failure catch |err| {
404 any_failures = true;
405 std.log.err("unable to hash '{s}': {s}", .{ hashed_file.path, @errorName(err) });
406 };
407 hasher.update(&hashed_file.hash);
408 }
409 if (any_failures) return error.PackageHashUnavailable;
410 return hasher.finalResult();
411}
412
413fn workerHashFile(dir: fs.Dir, hashed_file: *HashedFile, wg: *WaitGroup) void {
414 defer wg.finish();
415 hashed_file.failure = hashFileFallible(dir, hashed_file);
416}
417
418fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void {
419 var buf: [8000]u8 = undefined;
420 var file = try dir.openFile(hashed_file.path, .{});
421 var hasher = Hash.init(.{});
422 while (true) {
423 const bytes_read = try file.read(&buf);
424 if (bytes_read == 0) break;
425 hasher.update(buf[0..bytes_read]);
426 }
427 hasher.final(&hashed_file.hash);
428}
429
430const hex_charset = "0123456789abcdef";
431
432fn hex64(x: u64) [16]u8 {
433 var result: [16]u8 = undefined;
434 var i: usize = 0;
435 while (i < 8) : (i += 1) {
436 const byte = @truncate(u8, x >> @intCast(u6, 8 * i));
437 result[i * 2 + 0] = hex_charset[byte >> 4];
438 result[i * 2 + 1] = hex_charset[byte & 15];
439 }
440 return result;
441}
442
443test hex64 {
444 const s = "[" ++ hex64(0x12345678_abcdef00) ++ "]";
445 try std.testing.expectEqualStrings("[00efcdab78563412]", s);
446}
447
448fn hexDigest(digest: [Hash.digest_length]u8) [Hash.digest_length * 2]u8 {
449 var result: [Hash.digest_length * 2]u8 = undefined;
450 for (digest) |byte, i| {
451 result[i * 2 + 0] = hex_charset[byte >> 4];
452 result[i * 2 + 1] = hex_charset[byte & 15];
453 }
454 return result;
309455}