|
17 | 17 |
|
18 | 18 | const std = @import("std"); |
19 | 19 | const templates = @import("templates.zig"); |
| 20 | +const junction = @import("junction.zig"); |
20 | 21 |
|
21 | 22 | pub const generated_dir = ".native/build"; |
22 | 23 |
|
| 24 | +/// Where the cross-volume junction lives, relative to the app dir: a |
| 25 | +/// directory junction `.native/sdk` -> framework root lets the generated |
| 26 | +/// build graph reference an SDK on another Windows volume through a |
| 27 | +/// relative zon path (`../sdk` from the `.native/build` build root), since |
| 28 | +/// build.zig.zon rejects absolute paths and no `..` chain crosses volumes. |
| 29 | +pub const sdk_junction_dir = ".native/sdk"; |
| 30 | +const junction_dependency_path = "../sdk"; |
| 31 | + |
23 | 32 | pub const Error = error{ |
24 | 33 | MissingFramework, |
25 | 34 | AlreadyEjected, |
| 35 | + CrossVolumeFramework, |
| 36 | +}; |
| 37 | + |
| 38 | +/// How the framework path dependency reaches the zon file. Pure decision |
| 39 | +/// over the `std.fs.path.relative` result, so the cross-volume case is |
| 40 | +/// testable with fabricated drive-letter paths on every host. |
| 41 | +pub const DependencyRoute = union(enum) { |
| 42 | + /// Same volume (or any non-Windows host): the computed relative path |
| 43 | + /// goes into the zon verbatim, exactly as before. |
| 44 | + relative: []const u8, |
| 45 | + /// The framework sits on another Windows volume, where no relative |
| 46 | + /// path exists: the zon gets `../sdk` and the caller must create or |
| 47 | + /// refresh the `.native/sdk` junction. |
| 48 | + junction, |
26 | 49 | }; |
27 | 50 |
|
| 51 | +pub fn routeDependencyPath(dependency_path: []const u8) DependencyRoute { |
| 52 | + if (junction.crossesVolumes(dependency_path)) return .junction; |
| 53 | + return .{ .relative = if (dependency_path.len == 0) "." else dependency_path }; |
| 54 | +} |
| 55 | + |
| 56 | +/// The two user-level ways out when the junction bridge is unavailable |
| 57 | +/// (appended to every cross-volume teaching error). |
| 58 | +pub const cross_volume_ways_out = junction.cross_volume_ways_out; |
| 59 | + |
28 | 60 | /// Where the `native_sdk` framework checkout lives, for wiring the path |
29 | 61 | /// dependency of a generated or ejected build graph. Resolution order: |
30 | 62 | /// 1. NATIVE_SDK_PATH environment variable (explicit override; the npm |
@@ -112,9 +144,40 @@ pub fn ensureGeneratedBuild(allocator: std.mem.Allocator, io: std.Io, app_dir: [ |
112 | 144 | const dependency_path = try std.fs.path.relative(allocator, cwd, null, gen_real, framework_real); |
113 | 145 | defer allocator.free(dependency_path); |
114 | 146 |
|
| 147 | + const zon_dependency_path: []const u8 = switch (routeDependencyPath(dependency_path)) { |
| 148 | + .relative => |path| path, |
| 149 | + .junction => bridge: { |
| 150 | + // Cross-volume (drive letters or UNC shares): wire the zon |
| 151 | + // through a `.native/sdk` junction instead of the absolute |
| 152 | + // path Zig rejects. Refreshed on every generation, so an SDK |
| 153 | + // that moved or was reinstalled elsewhere is retargeted here. |
| 154 | + const native_dir_real = std.fs.path.dirname(gen_real).?; |
| 155 | + const app_real = std.fs.path.dirname(native_dir_real) orelse native_dir_real; |
| 156 | + const junction_path = try std.fs.path.join(allocator, &.{ native_dir_real, "sdk" }); |
| 157 | + defer allocator.free(junction_path); |
| 158 | + junction.ensure(allocator, io, junction_path, framework_real) catch |err| switch (err) { |
| 159 | + error.OutOfMemory, error.Canceled => |e| return e, |
| 160 | + else => { |
| 161 | + std.debug.print( |
| 162 | + \\cannot wire this app to the Native SDK: the app ({s}) |
| 163 | + \\and the SDK ({s}) |
| 164 | + \\sit on different Windows volumes, and build.zig.zon path |
| 165 | + \\dependencies must be relative — no relative path crosses |
| 166 | + \\volumes. The CLI bridges that with a directory junction at |
| 167 | + \\{s}, but creating it failed ({t}). |
| 168 | + \\ |
| 169 | + , .{ app_real, framework_real, sdk_junction_dir, err }); |
| 170 | + std.debug.print(cross_volume_ways_out, .{}); |
| 171 | + return error.CrossVolumeFramework; |
| 172 | + }, |
| 173 | + }; |
| 174 | + break :bridge junction_dependency_path; |
| 175 | + }, |
| 176 | + }; |
| 177 | + |
115 | 178 | const build_zig = try renderBuildZig(allocator, options.app_name, .generated); |
116 | 179 | defer allocator.free(build_zig); |
117 | | - const build_zon = try renderBuildZon(allocator, options.app_name, if (dependency_path.len == 0) "." else dependency_path, .generated); |
| 180 | + const build_zon = try renderBuildZon(allocator, options.app_name, zon_dependency_path, .generated); |
118 | 181 | defer allocator.free(build_zon); |
119 | 182 |
|
120 | 183 | var dir = try std.Io.Dir.cwd().openDir(io, gen_path, .{}); |
@@ -149,9 +212,30 @@ pub fn eject(allocator: std.mem.Allocator, io: std.Io, app_dir: []const u8, opti |
149 | 212 | const dependency_path = try std.fs.path.relative(allocator, cwd, null, app_real, framework_real); |
150 | 213 | defer allocator.free(dependency_path); |
151 | 214 |
|
| 215 | + const zon_dependency_path: []const u8 = switch (routeDependencyPath(dependency_path)) { |
| 216 | + .relative => |path| path, |
| 217 | + // No junction bridge for eject: the ejected build belongs to the |
| 218 | + // user and is driven by plain `zig build`, so the CLI would never |
| 219 | + // refresh a `.native/sdk` junction again — it would silently rot |
| 220 | + // the first time the SDK moves. Teach the constraint instead. |
| 221 | + .junction => { |
| 222 | + std.debug.print( |
| 223 | + \\cannot eject: the app ({s}) |
| 224 | + \\and the Native SDK ({s}) |
| 225 | + \\sit on different Windows volumes, and the ejected |
| 226 | + \\build.zig.zon needs a relative SDK path — no relative path |
| 227 | + \\crosses volumes, and an ejected build is user-owned, so the |
| 228 | + \\CLI cannot keep a junction bridge fresh for it. |
| 229 | + \\ |
| 230 | + , .{ app_real, framework_real }); |
| 231 | + std.debug.print(cross_volume_ways_out, .{}); |
| 232 | + return error.CrossVolumeFramework; |
| 233 | + }, |
| 234 | + }; |
| 235 | + |
152 | 236 | const build_zig = try renderBuildZig(allocator, options.app_name, .ejected); |
153 | 237 | defer allocator.free(build_zig); |
154 | | - const build_zon = try renderBuildZon(allocator, options.app_name, if (dependency_path.len == 0) "." else dependency_path, .ejected); |
| 238 | + const build_zon = try renderBuildZon(allocator, options.app_name, zon_dependency_path, .ejected); |
155 | 239 | defer allocator.free(build_zon); |
156 | 240 |
|
157 | 241 | try dir.writeFile(io, .{ .sub_path = "build.zig", .data = build_zig }); |
@@ -283,6 +367,32 @@ test "generated build.zig.zon wires the framework path dependency" { |
283 | 367 | try std.testing.expect(std.mem.indexOf(u8, text, ".paths = .{ \"build.zig\", \"build.zig.zon\" }") != null); |
284 | 368 | } |
285 | 369 |
|
| 370 | +test "dependency routing keeps relative paths and bridges cross-volume ones" { |
| 371 | + // Same volume (and every non-Windows host): verbatim, zero change. |
| 372 | + try std.testing.expectEqualStrings("../../../framework", routeDependencyPath("../../../framework").relative); |
| 373 | + try std.testing.expectEqualStrings("..\\..\\sdk", routeDependencyPath("..\\..\\sdk").relative); |
| 374 | + // Build root == framework root: relative() returns "", the zon gets ".". |
| 375 | + try std.testing.expectEqualStrings(".", routeDependencyPath("").relative); |
| 376 | + // Cross-volume: std.fs.path.relative degrades to the absolute target |
| 377 | + // (drive letters or UNC shares), which must route through the junction. |
| 378 | + try std.testing.expect(routeDependencyPath("C:\\Users\\alpha\\AppData\\Roaming\\npm\\node_modules\\@native-sdk\\cli") == .junction); |
| 379 | + try std.testing.expect(routeDependencyPath("\\\\server\\share\\native-sdk") == .junction); |
| 380 | +} |
| 381 | + |
| 382 | +test "the junction route renders a relative zon path through .native/sdk" { |
| 383 | + const text = try renderBuildZon(std.testing.allocator, "my-app", junction_dependency_path, .generated); |
| 384 | + defer std.testing.allocator.free(text); |
| 385 | + try std.testing.expect(std.mem.indexOf(u8, text, ".native_sdk = .{ .path = \"../sdk\" }") != null); |
| 386 | + // Never an absolute path in the zon, junction route included. |
| 387 | + try std.testing.expect(std.mem.indexOf(u8, text, ".path = \"C:") == null); |
| 388 | + try std.testing.expect(std.mem.indexOf(u8, text, ".path = \"\\\\") == null); |
| 389 | +} |
| 390 | + |
| 391 | +test "the cross-volume teaching text names both user-level ways out" { |
| 392 | + try std.testing.expect(std.mem.indexOf(u8, cross_volume_ways_out, "same volume") != null); |
| 393 | + try std.testing.expect(std.mem.indexOf(u8, cross_volume_ways_out, "npm config set prefix") != null); |
| 394 | +} |
| 395 | + |
286 | 396 | /// Path equality where '/' in the expected value also matches the |
287 | 397 | /// platform separator, so tests written with forward slashes hold on |
288 | 398 | /// Windows (where std.fs.path.join emits backslashes). |
|
0 commit comments