Skip to content

Commit 36d295a

Browse files
ctatefleeting-zone
andauthored
Route cross-volume SDK dependencies through a junction on Windows (#92)
* Route cross-volume SDK dependencies through a junction on Windows - A project and the npm-global SDK on different drives have no relative path, so the generated build graph now creates a .native/sdk directory junction and references the framework through it; same-volume layouts keep plain relative paths - Junctions are created via the NT reparse API (no admin rights), refreshed idempotently on retarget or dangling, and never replace real directories; ejected and full-scaffold builds get a teaching error instead since the CLI cannot keep a junction fresh in user-owned build files Co-authored-by: fleeting-zone <44354736+fleeting-zone@users.noreply.github.com> * Tidy fallback ownership and route cross-volume errors in mobile packaging - nativeDependencyPath dupes the dot fallback before freeing the empty relative path, so the errdefer owns it exactly once on every path - package --target ios and android exit quietly on CrossVolumeFramework instead of dumping an error trace after the teaching text; a failed junction means the generated project cannot build, so no libraryless package is produced --------- Co-authored-by: fleeting-zone <44354736+fleeting-zone@users.noreply.github.com>
1 parent 908e3de commit 36d295a

6 files changed

Lines changed: 433 additions & 4 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fix: **Cross-drive apps on Windows**: `native dev|build|test` no longer fails with `expected path relative to build root; found absolute path` when the app and the npm-installed SDK live on different drives — the generated build graph now bridges volumes with a `.native/sdk` directory junction (no admin rights needed) and keeps the zon dependency relative; the junction is retargeted automatically when the SDK moves or upgrades. Where the bridge cannot apply (`native eject`, full-shape `native init`, or a filesystem that refuses junctions), the CLI explains the cross-volume constraint and both ways out instead of writing a build Zig would reject.

src/tooling/buildgraph.zig

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,46 @@
1717

1818
const std = @import("std");
1919
const templates = @import("templates.zig");
20+
const junction = @import("junction.zig");
2021

2122
pub const generated_dir = ".native/build";
2223

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+
2332
pub const Error = error{
2433
MissingFramework,
2534
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,
2649
};
2750

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+
2860
/// Where the `native_sdk` framework checkout lives, for wiring the path
2961
/// dependency of a generated or ejected build graph. Resolution order:
3062
/// 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: [
112144
const dependency_path = try std.fs.path.relative(allocator, cwd, null, gen_real, framework_real);
113145
defer allocator.free(dependency_path);
114146

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+
115178
const build_zig = try renderBuildZig(allocator, options.app_name, .generated);
116179
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);
118181
defer allocator.free(build_zon);
119182

120183
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
149212
const dependency_path = try std.fs.path.relative(allocator, cwd, null, app_real, framework_real);
150213
defer allocator.free(dependency_path);
151214

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+
152236
const build_zig = try renderBuildZig(allocator, options.app_name, .ejected);
153237
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);
155239
defer allocator.free(build_zon);
156240

157241
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" {
283367
try std.testing.expect(std.mem.indexOf(u8, text, ".paths = .{ \"build.zig\", \"build.zig.zon\" }") != null);
284368
}
285369

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+
286396
/// Path equality where '/' in the expected value also matches the
287397
/// platform separator, so tests written with forward slashes hold on
288398
/// Windows (where std.fs.path.join emits backslashes).

0 commit comments

Comments
 (0)