ReviewOS

also looking at this

pantry-pm/pantry

fix(install): keep .env-inferred services out of pantry.lock

#254
Merged glennmichael123 wants to merge fix/253-env-deps-lockfile into main
4 files +286 -70
packages/zig/src/cli/commands/install/core.zigmodified+91-22
Changes to packages/zig/src/cli/commands/install/core.zig
@@ -167,11 +167,16 @@ fn tryFastUpToDate(allocator: std.mem.Allocator, cwd: []const u8, start_time: i6
167167
168168 // 4a. Services the project's .env names (DB_CONNECTION=pgsql, ...) count
169169 // as dependencies too, or a changed driver would never be installed.
170 //
171 // Asked of the disk rather than of the lockfile, because an inference is
172 // deliberately absent from the lock (#253). Against the lock this read
173 // "not locked, so not up to date" for every inferred service, sending a
174 // project with a `.env` down the slow path on every install.
170175 {
171176 const inferred = env_deps.infer(allocator, cwd, deps, .{ .report = false }) catch &.{};
172177 defer env_deps.freeDeps(allocator, inferred);
173178 for (inferred) |dep| {
174 if (!helpers.canSkipFromLockfile(&lockfile.packages, dep.name, dep.version, cwd, allocator, modules_dir)) return null;
179 if (!helpers.isPackageInstalled(cwd, modules_dir, dep.name)) return null;
175180 }
176181 }
177182
@@ -563,7 +568,6 @@ pub fn installCommandWithOptions(allocator: std.mem.Allocator, args: []const []c
563568 ws_file.root_dir,
564569 companion_options,
565570 staged_lockfile_path,
566 options.frozen_lockfile,
567571 ), df.path);
568572 }
569573 }
@@ -701,11 +705,20 @@ pub fn installCommandWithOptions(allocator: std.mem.Allocator, args: []const []c
701705 allocator,
702706 if (deps_file_path) |path| std.fs.path.dirname(path) orelse cwd else cwd,
703707 deps,
704 .{ .frozen_lockfile = options.frozen_lockfile },
708 .{},
705709 ) catch &.{};
706710 defer env_deps.freeDeps(allocator, env_inferred);
707711 try filtered_deps.appendSlice(allocator, env_inferred);
708712
713 // They are installed, and that is all they are. `pantry.lock` records
714 // what the workspace declares, so neither these nor what they pull in
715 // may reach it — a lock refreshed on a machine whose `.env` says
716 // postgres otherwise carries three entries a checkout without that
717 // `.env` removes again (#253).
718 var env_only_domains = env_deps.inferredOnlyDomains(allocator, env_inferred, deps) catch
719 env_deps.DomainSet{ .allocator = allocator, .map = std.StringHashMap(void).init(allocator) };
720 defer env_only_domains.deinit();
721
709722 // Load overrides/resolutions from package.json if it exists
710723 var override_map = lib.deps.overrides.OverrideMap.init(allocator);
711724 defer override_map.deinit();
@@ -1219,15 +1232,21 @@ pub fn installCommandWithOptions(allocator: std.mem.Allocator, args: []const []c
12191232 const resolved_url = try std.fmt.allocPrint(allocator, "registry:{s}@{s}", .{ clean_name, result.version });
12201233 defer allocator.free(resolved_url);
12211234
1222 if (options.frozen_lockfile) {
1223 if (!resolutionLockMatches(&lock_file, clean_name, result.version, resolved_url))
1224 frozen_lockfile_changed = true;
1225 } else if (!options.no_save) {
1226 if (lockfile_hooks.getLockedVersionForPackage(&lock_file, clean_name)) |previous| {
1227 if (!std.mem.eql(u8, previous.version, result.version))
1228 style.printLockPinMoved(clean_name, previous.version, result.version, false);
1235 // An inferred service is outside the lock's contract, so it
1236 // is neither written to the lock nor able to report the lock
1237 // as out of date. It is still installed, and the checkpoint
1238 // below still records it.
1239 if (!env_only_domains.contains(helpers.resolvePackageAlias(clean_name))) {
1240 if (options.frozen_lockfile) {
1241 if (!resolutionLockMatches(&lock_file, clean_name, result.version, resolved_url))
1242 frozen_lockfile_changed = true;
1243 } else if (!options.no_save) {
1244 if (lockfile_hooks.getLockedVersionForPackage(&lock_file, clean_name)) |previous| {
1245 if (!std.mem.eql(u8, previous.version, result.version))
1246 style.printLockPinMoved(clean_name, previous.version, result.version, false);
1247 }
1248 try lockfile_hooks.addPackageToLockfile(&lock_file, clean_name, result.version, resolved_url, null);
12291249 }
1230 try lockfile_hooks.addPackageToLockfile(&lock_file, clean_name, result.version, resolved_url, null);
12311250 }
12321251 // Nothing of it is on disk to record below.
12331252 if (result.lock_only) continue;
@@ -1975,7 +1994,6 @@ fn installCompanionDepsFile(
19751994 project_root: []const u8,
19761995 options: types.InstallOptions,
19771996 lockfile_path: ?[]const u8,
1978 frozen_environment: bool,
19791997) !types.CommandResult {
19801998 const parser = ;
19811999 const pipeline = ;
@@ -1989,13 +2007,10 @@ fn installCompanionDepsFile(
19892007 allocator.free(declared_deps);
19902008 }
19912009
1992 // Services the project's .env names, unless declared (#36). A non-null
1993 // Frozen installs infer only what the committed lock already records.
1994 // A staged path is also used by ordinary installs; it does not make their
1995 // environment inference frozen.
1996 const env_inferred = env_deps.infer(allocator, project_root, declared_deps, .{
1997 .frozen_lockfile = frozen_environment,
1998 }) catch &.{};
2010 // Services the project's .env names, unless declared (#36). What they
2011 // resolve to is installed but never locked (#253), so a frozen run infers
2012 // exactly what any other run does.
2013 const env_inferred = env_deps.infer(allocator, project_root, declared_deps, .{}) catch &.{};
19992014 defer env_deps.freeDeps(allocator, env_inferred);
20002015 const deps = if (env_inferred.len == 0) declared_deps else try std.mem.concat(allocator, parser.PackageDependency, &.{ declared_deps, env_inferred });
20012016 defer if (env_inferred.len > 0) allocator.free(deps);
@@ -2092,11 +2107,19 @@ fn installCompanionDepsFile(
20922107 // lockfile so `pantry install` actually refreshes stale runtime pins while
20932108 // preserving every workspace and npm entry.
20942109 if (!options.no_save) {
2110 // `declared_deps`, not `deps`: the constraints recorded on the root
2111 // workspace are the ones the workspace declares. `env_only_domains`
2112 // keeps the same inference out of the resolved package entries, where
2113 // it arrives with everything it pulled in.
2114 var env_only_domains = try env_deps.inferredOnlyDomains(allocator, env_inferred, declared_deps);
2115 defer env_only_domains.deinit();
2116
20952117 const lockfile_changed = try syncCompanionLockfile(
20962118 allocator,
20972119 project_root,
2098 deps,
2120 declared_deps,
20992121 pipeline_result.results,
2122 &env_only_domains,
21002123 options.frozen_lockfile,
21012124 lockfile_path,
21022125 );
@@ -2118,6 +2141,7 @@ fn syncCompanionLockfile(
21182141 project_root: []const u8,
21192142 deps: []const .PackageDependency,
21202143 results: []const .PackageResult,
2144 env_only_domains: *const env_deps.DomainSet,
21212145 frozen: bool,
21222146 lockfile_path_override: ?[]const u8,
21232147) !bool {
@@ -2138,7 +2162,7 @@ fn syncCompanionLockfile(
21382162 try lib.packages.Lockfile.init(allocator, "1.0.0");
21392163 defer lockfile.deinit(allocator);
21402164
2141 try mergeCompanionLockfileEntries(allocator, &lockfile, deps, results);
2165 try mergeCompanionLockfileEntries(allocator, &lockfile, deps, results, env_only_domains);
21422166
21432167 const changed = if (original) |*existing|
21442168 !lockfile_writer.lockfilesEqual(existing, &lockfile)
@@ -2161,6 +2185,7 @@ fn mergeCompanionLockfileEntries(
21612185 lockfile: *lib.packages.Lockfile,
21622186 deps: []const .PackageDependency,
21632187 results: []const .PackageResult,
2188 env_only_domains: *const env_deps.DomainSet,
21642189) !void {
21652190 // Packages whose resolved version depends on which platform installs them,
21662191 // so the sweep below can tell a stale pin from another platform's.
@@ -2218,6 +2243,7 @@ fn mergeCompanionLockfileEntries(
22182243 for (results) |result| {
22192244 if (!result.success or result.name.len == 0) continue;
22202245 const clean_name = helpers.resolvePackageAlias(helpers.normalizePackageName(result.name));
2246 if (env_only_domains.contains(clean_name)) continue;
22212247
22222248 // Replace every older pin for this package, regardless of its key's
22232249 // version spelling (constraint or resolved version).
@@ -2366,7 +2392,9 @@ test "companion deps replace stale system pins without dropping workspace packag
23662392 .success = true,
23672393 }};
23682394
2369 try mergeCompanionLockfileEntries(allocator, &lockfile, &deps, &results);
2395 var env_only = env_deps.DomainSet{ .allocator = allocator, .map = std.StringHashMap(void).init(allocator) };
2396 defer env_only.deinit();
2397 try mergeCompanionLockfileEntries(allocator, &lockfile, &deps, &results, &env_only);
23702398
23712399 try t.expectEqualStrings("^1.3.14", lockfile.workspaces.get("").?.system.?.get("bun.sh").?);
23722400 try t.expect(lockfile.packages.get("bun.sh@^1.3.4") == null);
@@ -2375,6 +2403,47 @@ test "companion deps replace stale system pins without dropping workspace packag
23752403 try t.expect(lockfile.workspaces.get("packages/ui") != null);
23762404}
23772405
2406test "a service inferred from .env stays out of the lock, with what it pulled in" {
2407 const t = std.testing;
2408 const allocator = t.allocator;
2409 const parser = ;
2410 const pipeline = ;
2411
2412 var lockfile = try lib.packages.Lockfile.init(allocator, "1.0.0");
2413 defer lockfile.deinit(allocator);
2414 try lockfile.addWorkspace(allocator, "", .{ .name = try allocator.dupe(u8, "fixture") });
2415
2416 // What the workspace declares. postgresql.org is not in it: `.env` said
2417 // DB_CONNECTION=postgres on the machine that ran this install.
2418 const declared = [_]parser.PackageDependency{.{ .name = "sqlite.org", .version = "^3" }};
2419 const inferred = [_]parser.PackageDependency{.{ .name = "postgresql.org", .version = "latest" }};
2420
2421 // The pipeline installed all of it, postgres's own dependencies included.
2422 const results = [_]pipeline.PackageResult{
2423 .{ .name = "sqlite.org", .version = "3.51.0", .success = true },
2424 .{ .name = "postgresql.org", .version = "18.4.0", .success = true },
2425 .{ .name = "gnome.org/libxslt", .version = "1.1.45", .success = true },
2426 .{ .name = "unicode.org", .version = "73.2.0", .success = true },
2427 };
2428
2429 var env_only = try env_deps.inferredOnlyDomains(allocator, &inferred, &declared);
2430 defer env_only.deinit();
2431 try mergeCompanionLockfileEntries(allocator, &lockfile, &declared, &results, &env_only);
2432
2433 // The declared package is locked, as always.
2434 try t.expectEqualStrings("3.51.0", lockfile.packages.get("sqlite.org@3.51.0").?.version);
2435 try t.expectEqualStrings("^3", lockfile.workspaces.get("").?.system.?.get("sqlite.org").?);
2436
2437 // The three entries the reported lock gained are absent, as is the
2438 // constraint that put "postgresql.org": "latest" on the root workspace —
2439 // so this lock is byte-identical to one cut on a machine whose .env says
2440 // sqlite (#253).
2441 try t.expect(lockfile.packages.get("postgresql.org@18.4.0") == null);
2442 try t.expect(lockfile.packages.get("gnome.org/libxslt@1.1.45") == null);
2443 try t.expect(lockfile.packages.get("unicode.org@73.2.0") == null);
2444 try t.expect(lockfile.workspaces.get("").?.system.?.get("postgresql.org") == null);
2445}
2446
23782447test "a manifest install fails when a package it asked for did not arrive" {
23792448 // The bug this pins: this path returned 0 unconditionally while every other
23802449 // install path returned 1, so `pantry install` printed "1 package(s) failed