feat(native): support standalone HTTP server binaries#148
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
📝 WalkthroughWalkthroughAdds a standalone-source execution path for Forge: a C-ABI entrypoint to run embedded Forge source, an embedded runtime module, and native build changes to emit C wrappers that link to ChangesStandalone HTTP Server Binaries
Sequence DiagramsequenceDiagram
participant Binary as Standalone Binary
participant FFI as forge_execute_source<br/>(C FFI)
participant Runtime as Embedded Runtime
participant Interp as Interpreter
participant Host as Host Runtime
participant Server as HTTP Server
Binary->>FFI: call(source_ptr, source_len, path_ptr, path_len, allow_run)
FFI->>FFI: validate pointers & lengths\ndecode UTF-8 inputs
FFI->>Runtime: execute_source_standalone(source, config)
Runtime->>Runtime: build Tokio runtime\nrun async execution
Runtime->>Interp: parse & interpret with deferred host
Interp->>Interp: produce RuntimePlan from metadata
Interp->>Host: runtime::host::launch(plan)
Host->>Server: start HTTP server (e.g. `@server`)
Server-->>Binary: accept requests (e.g. /ping)
Runtime-->>FFI: return Result
FFI-->>Binary: exit status (0/1)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 6/10 reviews remaining, refill in 23 minutes and 52 seconds. Comment |
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a80cb5f376
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if let Some(lib_dir) = find_libforge_dir() { | ||
| return build_standalone_source(source, source_path, allow_run, &lib_dir) | ||
| .map(|path| NativeBuildOutput::standalone(path)); |
There was a problem hiding this comment.
Fall back to CLI launcher when standalone link fails
When find_libforge_dir() returns a path, build_native_launcher now returns the result of build_standalone_source directly, so any link failure (e.g., incompatible libforge_lang.a, missing system linker inputs, or bad FORGE_LIB_DIR) hard-fails the build instead of producing the previous launcher binary. This is a regression for forge build --native: users who previously got a working launcher now get an error solely because a static library is discoverable but unusable. The function should attempt standalone first, then fall back to compile_launcher(...) on standalone build errors.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Line 12: The changelog entry for "Standalone source-runtime native binaries
for Forge servers" is missing the PR reference; update the bullet so it follows
the required format by appending the PR suffix like " ([`#148`](link))" to the end
of that line under the [Unreleased] section (i.e., modify the existing line that
begins "Standalone source-runtime native binaries for Forge servers" to include
the PR reference and URL).
In `@src/main.rs`:
- Around line 347-353: The code is passing a combined flag (cli.allow_run ||
build_allow_run) into compile_to_native_launcher which lets the top-level
--allow-run incorrectly override the Build subcommand's requires/conflicts
semantics; change the call to pass only the build-local flag (build_allow_run)
to compile_to_native_launcher so only the Build subcommand's allow_run controls
embedding runtime shell permissions, leaving cli.allow_run to affect only the
top-level process.
In `@src/native.rs`:
- Around line 678-704: Currently the test binds to an ephemeral port then drops
the listener (listener/port) which allows races; instead remove the bind/drop
handshake and implement a retry loop when launching the child server: pick a
random ephemeral port, write source using that port
(temp_root/source_path/source), build with build_standalone_source, start the
server, and if startup fails with an address-in-use/bind error retry a few times
with a freshly generated port/source pair before failing the test; reference the
listener/port variables to remove, and reuse temp_root/source_path and
build_standalone_source in the retry logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 6dd3f7b2-cdd5-4e10-aa72-87bff3bc0aa2
📒 Files selected for processing (7)
.planning/http-servers-standalone-binaries.plan.mdCHANGELOG.mdsrc/lib.rssrc/main.rssrc/native.rssrc/runtime/embedded.rssrc/runtime/mod.rs
|
|
||
| ### Added | ||
|
|
||
| - **Standalone source-runtime native binaries for Forge servers** — `forge build --native` now links against `libforge_lang.a` when available and emits a single executable that embeds Forge source and starts interpreter-only runtime features like `@server` without shelling out to the `forge` CLI. `--aot` remains bytecode/VM-only and continues to reject decorator-driven servers with guidance to use `--native`. |
There was a problem hiding this comment.
Add the PR link suffix to this changelog entry.
This entry is missing the required ([#148](...)) reference under [Unreleased].
Suggested fix
-- **Standalone source-runtime native binaries for Forge servers** — `forge build --native` now links against `libforge_lang.a` when available and emits a single executable that embeds Forge source and starts interpreter-only runtime features like `@server` without shelling out to the `forge` CLI. `--aot` remains bytecode/VM-only and continues to reject decorator-driven servers with guidance to use `--native`.
+- **Standalone source-runtime native binaries for Forge servers** — `forge build --native` now links against `libforge_lang.a` when available and emits a single executable that embeds Forge source and starts interpreter-only runtime features like `@server` without shelling out to the `forge` CLI. `--aot` remains bytecode/VM-only and continues to reject decorator-driven servers with guidance to use `--native`. ([`#148`](https://github.com/humancto/forge-lang/pull/148))As per coding guidelines, "Update CHANGELOG.md for every PR with user-facing changes, using the format - Description of change ([#PR](link)) under the [Unreleased] section."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - **Standalone source-runtime native binaries for Forge servers** — `forge build --native` now links against `libforge_lang.a` when available and emits a single executable that embeds Forge source and starts interpreter-only runtime features like `@server` without shelling out to the `forge` CLI. `--aot` remains bytecode/VM-only and continues to reject decorator-driven servers with guidance to use `--native`. | |
| - **Standalone source-runtime native binaries for Forge servers** — `forge build --native` now links against `libforge_lang.a` when available and emits a single executable that embeds Forge source and starts interpreter-only runtime features like `@server` without shelling out to the `forge` CLI. `--aot` remains bytecode/VM-only and continues to reject decorator-driven servers with guidance to use `--native`. ([`#148`](https://github.com/humancto/forge-lang/pull/148)) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@CHANGELOG.md` at line 12, The changelog entry for "Standalone source-runtime
native binaries for Forge servers" is missing the PR reference; update the
bullet so it follows the required format by appending the PR suffix like "
([`#148`](link))" to the end of that line under the [Unreleased] section (i.e.,
modify the existing line that begins "Standalone source-runtime native binaries
for Forge servers" to include the PR reference and URL).
| compile_to_native_launcher( | ||
| &source, | ||
| &path_str, | ||
| &file, | ||
| strict, | ||
| cli.allow_run || build_allow_run, | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's locate and examine src/main.rs
git ls-files | grep -E "^src/main\.rs$"Repository: humancto/forge-lang
Length of output: 75
🏁 Script executed:
# Check the file size and read the relevant sections
wc -l src/main.rsRepository: humancto/forge-lang
Length of output: 80
🏁 Script executed:
# Read the CLI struct and Build command definition
head -200 src/main.rs | tail -100Repository: humancto/forge-lang
Length of output: 3246
🏁 Script executed:
# Read lines 151-160 to see the allow_run flag definition
sed -n '151,160p' src/main.rsRepository: humancto/forge-lang
Length of output: 457
🏁 Script executed:
# Read lines 324-360 to see the build dispatch and the line in question
sed -n '324,360p' src/main.rsRepository: humancto/forge-lang
Length of output: 1284
🏁 Script executed:
# Check if compile_to_native_aot takes an allow_run parameter
rg -A 5 "fn compile_to_native_aot" src/main.rsRepository: humancto/forge-lang
Length of output: 366
🏁 Script executed:
# Check if compile_to_native_launcher takes an allow_run parameter
rg -A 5 "fn compile_to_native_launcher" src/main.rsRepository: humancto/forge-lang
Length of output: 196
🏁 Script executed:
# Check how compile_to_bytecode is implemented and if it uses the flag
rg -A 5 "fn compile_to_bytecode" src/main.rsRepository: humancto/forge-lang
Length of output: 364
🏁 Script executed:
# Let's also see the full context around the Build command dispatch to understand the flow
sed -n '324,355p' src/main.rsRepository: humancto/forge-lang
Length of output: 1164
🏁 Script executed:
# Check all calls to compile_to_native_launcher to see if there are other places using it
rg "compile_to_native_launcher" src/main.rsRepository: humancto/forge-lang
Length of output: 138
🏁 Script executed:
# Let me verify the actual behavior by checking what the top-level allow_run affects
rg -B 5 -A 10 "Some(Command::Run" src/main.rs | head -40Repository: humancto/forge-lang
Length of output: 136
🏁 Script executed:
# Let's also check if there are any tests or documentation about this flag behavior
find . -name "*.md" -o -name "*.rs" | xargs rg "allow-run|allow_run" | head -20Repository: humancto/forge-lang
Length of output: 1326
🏁 Script executed:
# Check the Run command to see how it uses allow_run
sed -n '1,330p' src/main.rs | grep -A 20 "Some(Command::Run"Repository: humancto/forge-lang
Length of output: 1092
🏁 Script executed:
# Find where permissions::set_allow_run is called and how it's used
rg -B 3 -A 3 "permissions::set_allow_run" src/main.rsRepository: humancto/forge-lang
Length of output: 393
🏁 Script executed:
# Check if there are any comments or documentation about the semantic difference
rg -B 5 "cli.allow_run" src/main.rsRepository: humancto/forge-lang
Length of output: 630
🏁 Script executed:
# Let me trace the exact flow - check if compile_to_native_launcher is called anywhere else
rg -n "compile_to_native_launcher" src/main.rsRepository: humancto/forge-lang
Length of output: 146
🏁 Script executed:
# Verify the exact structure and branching logic around the build dispatch
sed -n '340,360p' src/main.rsRepository: humancto/forge-lang
Length of output: 728
🏁 Script executed:
# Check what the Build command allow_run constraint actually does in Clap
sed -n '108,118p' src/main.rsRepository: humancto/forge-lang
Length of output: 397
Remove the top-level --allow-run bypass in the build dispatch.
The Build subcommand's allow_run field explicitly declares requires = "native" and conflicts_with = "aot", establishing that shell execution permission embedding is only valid for native builds. However, cli.allow_run || build_allow_run allows the top-level flag to bypass this constraint—a user can pass forge --allow-run build --aot app.fg and the top-level permission will be OR'd into the decision.
While the aot branch doesn't use the allow_run parameter (so there's no functional bug), this violates the documented semantic contract. The top-level --allow-run controls runtime permissions for the forge process itself, whereas the build subcommand's --allow-run controls whether to embed permissions in the compiled binary—these are separate concerns and should not be conflated. Only the build-local flag should apply to the build output.
Suggested fix
compile_to_native_launcher(
&source,
&path_str,
&file,
strict,
- cli.allow_run || build_allow_run,
+ build_allow_run,
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| compile_to_native_launcher( | |
| &source, | |
| &path_str, | |
| &file, | |
| strict, | |
| cli.allow_run || build_allow_run, | |
| ); | |
| compile_to_native_launcher( | |
| &source, | |
| &path_str, | |
| &file, | |
| strict, | |
| build_allow_run, | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main.rs` around lines 347 - 353, The code is passing a combined flag
(cli.allow_run || build_allow_run) into compile_to_native_launcher which lets
the top-level --allow-run incorrectly override the Build subcommand's
requires/conflicts semantics; change the call to pass only the build-local flag
(build_allow_run) to compile_to_native_launcher so only the Build subcommand's
allow_run controls embedding runtime shell permissions, leaving cli.allow_run to
affect only the top-level process.
| let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind ephemeral port"); | ||
| let port = listener.local_addr().unwrap().port(); | ||
| drop(listener); | ||
|
|
||
| let temp_root = std::env::temp_dir().join(format!( | ||
| "forge-native-server-test-{}", | ||
| SystemTime::now() | ||
| .duration_since(UNIX_EPOCH) | ||
| .unwrap() | ||
| .as_nanos() | ||
| )); | ||
| std::fs::create_dir_all(&temp_root).unwrap(); | ||
| let source_path = temp_root.join("server.fg"); | ||
| let source = format!( | ||
| r#" | ||
| @server(port: {port}) | ||
|
|
||
| @get("/ping") | ||
| fn ping() -> Json {{ | ||
| return {{ ok: true }} | ||
| }} | ||
| "# | ||
| ); | ||
| std::fs::write(&source_path, &source).unwrap(); | ||
|
|
||
| let output_path = | ||
| build_standalone_source(&source, &source_path, false, &lib_dir).expect("build server"); |
There was a problem hiding this comment.
Avoid the bind/drop/rebind port handoff in this smoke test.
The test releases the ephemeral port before the child process binds it, so another process can steal that port and make this test fail nondeterministically in CI. Retrying with a freshly generated source/port pair on startup failure would make the smoke test much less flaky.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/native.rs` around lines 678 - 704, Currently the test binds to an
ephemeral port then drops the listener (listener/port) which allows races;
instead remove the bind/drop handshake and implement a retry loop when launching
the child server: pick a random ephemeral port, write source using that port
(temp_root/source_path/source), build with build_standalone_source, start the
server, and if startup fails with an address-in-use/bind error retry a few times
with a freshly generated port/source pair before failing the test; reference the
listener/port variables to remove, and reuse temp_root/source_path and
build_standalone_source in the retry logic.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/main.rs (1)
347-353:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse only build-local
--allow-runwhen embedding native permissions.Line 352 still ORs top-level and build-local flags. That bypasses the
build --allow-runscoping and can unintentionally bake shell permission into produced native binaries.Suggested fix
compile_to_native_launcher( &source, &path_str, &file, strict, - cli.allow_run || build_allow_run, + build_allow_run, );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main.rs` around lines 347 - 353, The call to compile_to_native_launcher is incorrectly combining top-level and build-local run permissions (cli.allow_run || build_allow_run), which can leak top-level --allow-run into embedded native binaries; update the call so it only uses the build-local flag (build_allow_run) when embedding native permissions by replacing the OR expression with build_allow_run so compile_to_native_launcher(...) receives the scoped build-local value only.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/main.rs`:
- Around line 347-353: The call to compile_to_native_launcher is incorrectly
combining top-level and build-local run permissions (cli.allow_run ||
build_allow_run), which can leak top-level --allow-run into embedded native
binaries; update the call so it only uses the build-local flag (build_allow_run)
when embedding native permissions by replacing the OR expression with
build_allow_run so compile_to_native_launcher(...) receives the scoped
build-local value only.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: eabe6aae-80af-4c31-948b-1ae098b07c59
📒 Files selected for processing (3)
src/main.rssrc/native.rssrc/runtime/embedded.rs
✅ Files skipped from review due to trivial changes (1)
- src/native.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- src/runtime/embedded.rs
Summary
@serverin-process without shelling out toforge.forge build --nativeto emit a standalone source-runtime wrapper whenlibforge_lang.ais available, while preserving CLI launcher fallback when it is not.--aothonest as bytecode/VM-only and adds guidance for decorated server programs to use--native.Roadmap
ROADMAP.md:HTTP servers work as standalone binaries.Test plan
Made with Cursor
Summary by CodeRabbit
New Features
forge build --nativecan produce a single standalone executable that embeds Forge source and runs without the CLI.--allow-runto gate shell/run permissions for native builds.--native).Documentation
Tests