Merge pull request #8461 from roc-lang/fix-relative-paths

Fix relative paths to platforms
This commit is contained in:
Richard Feldman 2025-11-26 17:02:13 -05:00 committed by GitHub
commit 719b878da4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 100 additions and 12 deletions

View file

@ -1353,22 +1353,37 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons
defer builtin_modules.deinit();
// Try to find and compile platform modules
const app_dir = std.fs.path.dirname(roc_file_path) orelse ".";
const platform_dir = try std.fs.path.join(allocs.gpa, &[_][]const u8{ app_dir, "platform" });
defer allocs.gpa.free(platform_dir);
// Extract the actual platform path from the app header to support paths like "../platform/main.roc"
const app_dir = std.fs.path.dirname(roc_file_path) orelse return error.InvalidAppPath;
const platform_main_path = try std.fs.path.join(allocs.gpa, &[_][]const u8{ platform_dir, "main.roc" });
defer allocs.gpa.free(platform_main_path);
const platform_spec = try extractPlatformSpecFromApp(allocs, roc_file_path);
// Resolve relative paths (starting with ./ or ../) relative to app directory
// Non-relative paths (like package URLs) are not local platform files
const platform_main_path: ?[]const u8 = if (std.mem.startsWith(u8, platform_spec, "./") or std.mem.startsWith(u8, platform_spec, "../"))
try std.fs.path.join(allocs.gpa, &[_][]const u8{ app_dir, platform_spec })
else
null;
defer if (platform_main_path) |p| allocs.gpa.free(p);
// Get the platform directory from the resolved path
const platform_dir: ?[]const u8 = if (platform_main_path) |p|
std.fs.path.dirname(p) orelse return error.InvalidPlatformPath
else
null;
// Extract exposed modules from the platform header (if platform exists)
var exposed_modules = std.ArrayList([]const u8).empty;
defer exposed_modules.deinit(allocs.gpa);
var has_platform = true;
extractExposedModulesFromPlatform(allocs, platform_main_path, &exposed_modules) catch {
// No platform found - that's fine, just continue with no platform modules
has_platform = false;
};
var has_platform = false;
if (platform_main_path) |pmp| {
has_platform = true;
extractExposedModulesFromPlatform(allocs, pmp, &exposed_modules) catch {
// Platform file not found or couldn't be parsed - continue without platform modules
has_platform = false;
};
}
// IMPORTANT: Create header FIRST before any module compilation.
// The interpreter_shim expects the Header to be at FIRST_ALLOC_OFFSET (504).
@ -1402,10 +1417,13 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons
defer allocs.gpa.free(platform_env_ptrs);
for (exposed_modules.items, 0..) |module_name, i| {
// platform_dir is guaranteed to be non-null if exposed_modules is non-empty
// because we only populate exposed_modules when platform_main_path is non-null
const plat_dir = platform_dir orelse unreachable;
const module_filename = try std.fmt.allocPrint(allocs.gpa, "{s}.roc", .{module_name});
defer allocs.gpa.free(module_filename);
const module_path = try std.fs.path.join(allocs.gpa, &[_][]const u8{ platform_dir, module_filename });
const module_path = try std.fs.path.join(allocs.gpa, &[_][]const u8{ plat_dir, module_filename });
defer allocs.gpa.free(module_path);
const module_env_ptr = try compileModuleToSharedMemory(
@ -1459,9 +1477,10 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons
if (has_platform) {
// Cast []*ModuleEnv to []const *ModuleEnv for the function parameter
const const_platform_env_ptrs: []const *ModuleEnv = platform_env_ptrs;
// platform_main_path is guaranteed non-null when has_platform is true
platform_main_env = compileModuleToSharedMemory(
allocs,
platform_main_path,
platform_main_path.?,
"main.roc",
shm_allocator,
&builtin_modules,

View file

@ -119,6 +119,60 @@ test "fx platform effectful functions" {
try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Line 3 to stdout") == null);
}
test "fx platform with dotdot starting path" {
const allocator = testing.allocator;
try ensureRocBinary(allocator);
// Run the app from a subdirectory that uses ../ at the START of its platform path
// This tests that relative paths starting with .. are handled correctly
// Bug: paths starting with ../ fail with TypeMismatch, while ./path/../ works
const run_result = try std.process.Child.run(.{
.allocator = allocator,
.argv = &[_][]const u8{
"./zig-out/bin/roc",
"test/fx/subdir/app.roc",
},
});
defer allocator.free(run_result.stdout);
defer allocator.free(run_result.stderr);
switch (run_result.term) {
.Exited => |code| {
if (code != 0) {
std.debug.print("Run failed with exit code {}\n", .{code});
std.debug.print("STDOUT: {s}\n", .{run_result.stdout});
std.debug.print("STDERR: {s}\n", .{run_result.stderr});
return error.RunFailed;
}
},
else => {
std.debug.print("Run terminated abnormally: {}\n", .{run_result.term});
std.debug.print("STDOUT: {s}\n", .{run_result.stdout});
std.debug.print("STDERR: {s}\n", .{run_result.stderr});
return error.RunFailed;
},
}
// Verify stdout contains expected messages
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Hello from stdout!") != null);
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Line 1 to stdout") != null);
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Line 3 to stdout") != null);
// Verify stderr contains expected messages
try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Error from stderr!") != null);
try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Line 2 to stderr") != null);
// Verify stderr messages are NOT in stdout
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Error from stderr!") == null);
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Line 2 to stderr") == null);
// Verify stdout messages are NOT in stderr
try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Hello from stdout!") == null);
try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Line 1 to stdout") == null);
try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Line 3 to stdout") == null);
}
test "fx platform stdin to stdout" {
const allocator = testing.allocator;

15
test/fx/subdir/app.roc Normal file
View file

@ -0,0 +1,15 @@
app [main!] { pf: platform "../platform/main.roc" }
import pf.Stdout
import pf.Stderr
str : Str -> Str
str = |s| s
main! = || {
Stdout.line!(str("Hello from stdout!"))
Stdout.line!(str("Line 1 to stdout"))
Stderr.line!(str("Line 2 to stderr"))
Stdout.line!(str("Line 3 to stdout"))
Stderr.line!(str("Error from stderr!"))
}