@@ -96,7 +96,14 @@ const PathUtils = struct {
9696
9797 fn isWithinRoot (candidate : []const u8 , roots : []const []const u8 ) bool {
9898 for (roots ) | root | {
99- if (std .mem .startsWith (u8 , candidate , root )) return true ;
99+ if (! std .mem .startsWith (u8 , candidate , root )) continue ;
100+ // Only match whole path segments so root "/x/app" does not capture
101+ // sibling "/x/app-secrets". An exact match counts, as does a match
102+ // where the next character (or the root's own trailing char) is a
103+ // path separator.
104+ if (candidate .len == root .len ) return true ;
105+ if (std .fs .path .isSep (candidate [root .len ])) return true ;
106+ if (root .len > 0 and std .fs .path .isSep (root [root .len - 1 ])) return true ;
100107 }
101108 return false ;
102109 }
@@ -426,6 +433,13 @@ pub const BuildEnv = struct {
426433 self .coordinator = coord ;
427434 }
428435
436+ /// Register a directory as a workspace root, skipping it if an existing root
437+ /// already contains it. Takes ownership of nothing; copies `dir` when stored.
438+ fn addWorkspaceRoot (self : * BuildEnv , dir : []const u8 ) ! void {
439+ if (PathUtils .isWithinRoot (dir , self .workspace_roots .items )) return ;
440+ try self .workspace_roots .append (try self .gpa .dupe (u8 , dir ));
441+ }
442+
429443 /// Phase 1: Parse headers, create package entries, extract TargetsConfig, and populate
430444 /// shorthands. Does NOT init the Coordinator, allowing the caller to inspect
431445 /// discovered state (e.g., TargetsConfig) and change the target before compilation.
@@ -437,7 +451,7 @@ pub const BuildEnv = struct {
437451 const root_dir = if (std .fs .path .dirname (root_abs )) | d | try std .fs .path .resolve (self .gpa , &.{d }) else try self .gpa .dupe (u8 , "." );
438452 self .discovered_root_dir = root_dir ;
439453
440- try self .workspace_roots . append ( try self . gpa . dupe ( u8 , root_dir ) );
454+ try self .addWorkspaceRoot ( root_dir );
441455
442456 var header_info = try self .parseHeaderDeps (root_abs );
443457 defer header_info .deinit (self .gpa );
@@ -1089,9 +1103,7 @@ pub const BuildEnv = struct {
10891103 // can be resolved. This is needed for both URL packages (cached paths) and
10901104 // relative paths that may point outside the app directory (e.g., ../platform/main.roc)
10911105 if (std .fs .path .dirname (plat_path )) | plat_dir | {
1092- if (! PathUtils .isWithinRoot (plat_dir , self .workspace_roots .items )) {
1093- try self .workspace_roots .append (try self .gpa .dupe (u8 , plat_dir ));
1094- }
1106+ try self .addWorkspaceRoot (plat_dir );
10951107 }
10961108
10971109 // Packages map
@@ -1112,12 +1124,17 @@ pub const BuildEnv = struct {
11121124 const cached_path = try self .resolveUrlPackage (relp );
11131125 // Add cache directory to workspace roots for URL packages
11141126 if (std .fs .path .dirname (cached_path )) | cache_pkg_dir | {
1115- try self .workspace_roots . append ( try self . gpa . dupe ( u8 , cache_pkg_dir ) );
1127+ try self .addWorkspaceRoot ( cache_pkg_dir );
11161128 }
11171129 break :blk cached_path ;
11181130 } else blk : {
11191131 const header_dir2 = std .fs .path .dirname (file_abs ) orelse "." ;
1120- break :blk try PathUtils .makeAbsolute (self .gpa , header_dir2 , relp );
1132+ const abs_path = try PathUtils .makeAbsolute (self .gpa , header_dir2 , relp );
1133+ errdefer self .gpa .free (abs_path );
1134+ if (std .fs .path .dirname (abs_path )) | pkg_dir | {
1135+ try self .addWorkspaceRoot (pkg_dir );
1136+ }
1137+ break :blk abs_path ;
11211138 };
11221139
11231140 // TODO: actually handle duplicate keys
@@ -1147,7 +1164,7 @@ pub const BuildEnv = struct {
11471164 const cached_path = try self .resolveUrlPackage (relp );
11481165 // Add cache directory to workspace roots for URL packages
11491166 if (std .fs .path .dirname (cached_path )) | cache_pkg_dir | {
1150- try self .workspace_roots . append ( try self . gpa . dupe ( u8 , cache_pkg_dir ) );
1167+ try self .addWorkspaceRoot ( cache_pkg_dir );
11511168 }
11521169 break :blk cached_path ;
11531170 } else blk : {
@@ -1188,7 +1205,7 @@ pub const BuildEnv = struct {
11881205 const cached_path = try self .resolveUrlPackage (relp );
11891206 // Add cache directory to workspace roots for URL packages
11901207 if (std .fs .path .dirname (cached_path )) | cache_pkg_dir | {
1191- try self .workspace_roots . append ( try self . gpa . dupe ( u8 , cache_pkg_dir ) );
1208+ try self .addWorkspaceRoot ( cache_pkg_dir );
11921209 }
11931210 break :blk cached_path ;
11941211 } else blk : {
@@ -1502,6 +1519,29 @@ pub const BuildEnv = struct {
15021519 });
15031520 }
15041521
1522+ fn putPackageShorthand (self : * BuildEnv , pack : * Package , alias : []const u8 , target_name : []const u8 , root_file : []const u8 ) ! []const u8 {
1523+ const key = try self .gpa .dupe (u8 , alias );
1524+ errdefer self .gpa .free (key );
1525+
1526+ const name = try self .gpa .dupe (u8 , target_name );
1527+ errdefer self .gpa .free (name );
1528+
1529+ const root_file_owned = try self .gpa .dupe (u8 , root_file );
1530+ errdefer self .gpa .free (root_file_owned );
1531+
1532+ if (pack .shorthands .fetchRemove (key )) | old_entry | {
1533+ freeConstSlice (self .gpa , old_entry .key );
1534+ freeConstSlice (self .gpa , old_entry .value .name );
1535+ freeConstSlice (self .gpa , old_entry .value .root_file );
1536+ }
1537+ try pack .shorthands .put (self .gpa , key , .{
1538+ .name = name ,
1539+ .root_file = root_file_owned ,
1540+ });
1541+
1542+ return name ;
1543+ }
1544+
15051545 const PkgSinkCtx = struct {
15061546 gpa : Allocator ,
15071547 sink : * OrderedSink ,
@@ -1594,9 +1634,7 @@ pub const BuildEnv = struct {
15941634 return error .InvalidDependency ;
15951635 }
15961636
1597- const dep_key = try self .gpa .dupe (u8 , alias );
1598- const dep_name = try self .gpa .dupe (u8 , alias );
1599- try self .ensurePackage (dep_name , .platform , abs );
1637+ try self .ensurePackage (alias , .platform , abs );
16001638
16011639 // Transfer provides entries and targets_config from parsed header to platform package
16021640 if (self .packages .getPtr (alias )) | plat_pkg | {
@@ -1613,17 +1651,7 @@ pub const BuildEnv = struct {
16131651 // Re-fetch pack pointer since ensurePackage may have caused HashMap reallocation
16141652 pack = self .packages .getPtr (pkg_name ).? ;
16151653
1616- // If key already exists, free the old value before overwriting
1617- if (pack .shorthands .fetchRemove (dep_key )) | old_entry | {
1618- freeConstSlice (self .gpa , old_entry .key );
1619- freeConstSlice (self .gpa , old_entry .value .name );
1620- freeConstSlice (self .gpa , old_entry .value .root_file );
1621- }
1622- try pack .shorthands .put (self .gpa , dep_key , .{
1623- .name = dep_name ,
1624- .root_file = try self .gpa .dupe (u8 , abs ),
1625- });
1626-
1654+ const dep_name = try self .putPackageShorthand (pack , alias , alias , abs );
16271655 try self .populatePackageShorthands (dep_name , & child_info );
16281656
16291657 // Register platform-exposed modules as packages so apps can import them
@@ -1648,16 +1676,7 @@ pub const BuildEnv = struct {
16481676 pack = self .packages .getPtr (pkg_name ).? ;
16491677
16501678 // Also add to app's shorthands so imports resolve correctly
1651- const mod_key = try self .gpa .dupe (u8 , module_name );
1652- if (pack .shorthands .fetchRemove (mod_key )) | old_entry | {
1653- freeConstSlice (self .gpa , old_entry .key );
1654- freeConstSlice (self .gpa , old_entry .value .name );
1655- freeConstSlice (self .gpa , old_entry .value .root_file );
1656- }
1657- try pack .shorthands .put (self .gpa , mod_key , .{
1658- .name = try self .gpa .dupe (u8 , module_name ),
1659- .root_file = try self .gpa .dupe (u8 , module_path ),
1660- });
1679+ _ = try self .putPackageShorthand (pack , module_name , module_name , module_path );
16611680
16621681 // Add to pending list - will be registered after schedulers are created
16631682 // Use the QUALIFIED name (e.g., "pf.Stdout") because that's how imports are tracked
@@ -1704,10 +1723,7 @@ pub const BuildEnv = struct {
17041723 return error .InvalidDependency ;
17051724 }
17061725
1707- const dep_key = try self .gpa .dupe (u8 , alias );
1708- const dep_name = try self .gpa .dupe (u8 , alias );
1709-
1710- try self .ensurePackage (dep_name , child_info .kind , abs );
1726+ try self .ensurePackage (alias , child_info .kind , abs );
17111727
17121728 // Transfer provides entries from parsed header to platform package
17131729 if (child_info .kind == .platform ) {
@@ -1722,17 +1738,7 @@ pub const BuildEnv = struct {
17221738 // Re-fetch pack pointer since ensurePackage may have caused HashMap reallocation
17231739 pack = self .packages .getPtr (pkg_name ).? ;
17241740
1725- // If key already exists, free the old value before overwriting
1726- if (pack .shorthands .fetchRemove (dep_key )) | old_entry | {
1727- freeConstSlice (self .gpa , old_entry .key );
1728- freeConstSlice (self .gpa , old_entry .value .name );
1729- freeConstSlice (self .gpa , old_entry .value .root_file );
1730- }
1731- try pack .shorthands .put (self .gpa , dep_key , .{
1732- .name = dep_name ,
1733- .root_file = try self .gpa .dupe (u8 , abs ),
1734- });
1735-
1741+ const dep_name = try self .putPackageShorthand (pack , alias , alias , abs );
17361742 try self .populatePackageShorthands (dep_name , & child_info );
17371743 }
17381744 }
0 commit comments