Skip to content

feat(core): support building for iOS targets - #1246

Open
Jackkakaya wants to merge 2 commits into
reubeno:mainfrom
Jackkakaya:ios-target-support
Open

feat(core): support building for iOS targets#1246
Jackkakaya wants to merge 2 commits into
reubeno:mainfrom
Jackkakaya:ios-target-support

Conversation

@Jackkakaya

Copy link
Copy Markdown

What

Makes brush-core compile (and run) for iOS targets (aarch64-apple-ios, aarch64-apple-ios-sim). Three small, cfg-gated changes:

  1. uzers → non-iOS unix only, new users_ios module. uzers fails to compile for iOS, and the iOS app sandbox has no user database to query anyway. The new module answers identity queries from getuid()/getgid()-family syscalls and the process environment ($HOME, $USER).
  2. waitidwaitpid fallback on iOS. nix does not expose waitid() on iOS; this reuses the exact fallback path already in place for NetBSD/OpenBSD.
  3. ioctl(TIOCSCTTY) cfg widened from target_os = "macos" to target_vendor = "apple". The request argument is c_ulong on iOS just like macOS.

Why

brush-core turns out to be a remarkably good fit for embedding a shell inside an iOS app, where fork/exec is forbidden: working dir and env are per-Shell-instance state rather than process globals, builtins do I/O through the shell's fd table (real pipes) rather than process stdio, and any command name can be routed to an in-process builtin. With just this patch, brush-core powers a working iOS terminal app.

Testing

  • cargo check + cargo clippy clean for aarch64-apple-ios, aarch64-apple-ios-sim, and the host (macOS).
  • No behavior change on currently supported platforms: everything is cfg-gated to iOS except the ioctl cfg widening, which is value-identical on macOS.
  • Ran a 102-case shell battery (language features, redirections/pipes, ~35 builtins, custom in-process commands) on an iOS 17 simulator inside a demo app embedding brush-core: 102/102 pass.

Happy to also add an iOS check target to CI in a follow-up if that's welcome.

🤖 Generated with Claude Code

https://claude.ai/code/session_011tLwxQ47SiGLGDDPXd8tzz

brush-core currently fails to compile for aarch64-apple-ios /
aarch64-apple-ios-sim, even though the crate's instance-scoped design
(per-Shell working dir/env, fd-table-based I/O) otherwise works well
inside an iOS app. Three small platform issues block the build:

- uzers does not compile for iOS, and there is no user database inside
  the iOS app sandbox anyway. Gate the dependency to non-iOS unix and
  add a users_ios module that answers identity queries from uid/gid
  syscalls and the process environment.
- nix does not expose waitid() on iOS; take the same waitpid() fallback
  already used for NetBSD/OpenBSD.
- the ioctl(TIOCSCTTY) request argument is c_ulong on iOS just like
  macOS; widen the cfg from target_os = "macos" to
  target_vendor = "apple".

No behavior change on any currently supported platform: all edits are
cfg-gated to iOS except the ioctl cfg widening, which is identical on
macOS. cargo check/clippy pass for aarch64-apple-ios,
aarch64-apple-ios-sim, and the host.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011tLwxQ47SiGLGDDPXd8tzz
@reubeno

reubeno commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Thanks for the contribution, @Jackkakaya! It's great to hear that you were able to make use of brush-core on another platform 😄

I'll need to look a bit more closely at users_ios.rs; the other targeted cfg changes look reasonable. I also agree that it would make sense to follow up with some CI-time checks to make sure that things don't regress. I'm open to that either being in this PR or being in a fast-follow subsequent PR.

Lets embedders distinguish a redirected/piped fd from an inherited one
(e.g. tell a piped stdin from an interactive terminal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

Public API changes for crate: brush-core

Added items

+pub fn brush_core::ExecutionParameters::is_fd_specified(&self, brush_core::ShellFd) -> bool

Performance Benchmark Report

Benchmark name Baseline (μs) Test/PR (μs) Delta (μs) Delta %
clone_shell_object 18.20 μs 18.06 μs -0.14 μs ⚪ Unchanged
eval_arithmetic 0.15 μs 0.15 μs 0.00 μs ⚪ Unchanged
expand_one_string 1.66 μs 1.66 μs 0.00 μs ⚪ Unchanged
for_loop 32.72 μs 32.85 μs 0.13 μs ⚪ Unchanged
full_peg_complex 55.80 μs 56.14 μs 0.35 μs ⚪ Unchanged
full_peg_for_loop 6.16 μs 6.17 μs 0.00 μs ⚪ Unchanged
full_peg_nested_expansions 16.06 μs 16.14 μs 0.09 μs ⚪ Unchanged
full_peg_pipeline 4.16 μs 4.15 μs -0.01 μs ⚪ Unchanged
full_peg_simple 1.76 μs 1.80 μs 0.04 μs 🟠 +2.10%
function_call 3.51 μs 3.55 μs 0.04 μs ⚪ Unchanged
instantiate_shell 57.64 μs 57.11 μs -0.52 μs ⚪ Unchanged
instantiate_shell_with_init_scripts 27703.52 μs 27683.35 μs -20.17 μs ⚪ Unchanged
parse_peg_bash_completion 2096.07 μs 2088.10 μs -7.97 μs ⚪ Unchanged
parse_peg_complex 18.54 μs 18.71 μs 0.16 μs ⚪ Unchanged
parse_peg_for_loop 1.83 μs 1.80 μs -0.03 μs 🟢 -1.48%
parse_peg_pipeline 2.06 μs 1.92 μs -0.14 μs 🟢 -6.83%
parse_peg_simple 1.02 μs 1.03 μs 0.01 μs 🟠 +0.98%
run_echo_builtin_command 16.15 μs 16.12 μs -0.03 μs ⚪ Unchanged
tokenize_sample_script 3.55 μs 3.53 μs -0.02 μs ⚪ Unchanged

Code Coverage Report: Only Changed Files listed

Package Base Coverage New Coverage Difference
brush-core/src/interp.rs 🟢 93.02% 🟢 92.6% 🔴 -0.42%
Overall Coverage 🟢 76.12% 🟢 76.11% 🔴 -0.01%

Minimum allowed coverage is 70%, this run produced 76.11%
Maximum allowed coverage difference is -5%, this run produced -0.01%

Test Summary: bash-completion test suite

Outcome Count Percentage
✅ Pass 1582 75.01
❗️ Error 17 0.81
❌ Fail 156 7.40
⏩ Skip 339 16.07
❎ Expected Fail 13 0.62
✔️ Unexpected Pass 2 0.09
📊 Total 2109 100.00

@reubeno reubeno left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes generally looking good -- just a few requests/questions.

#[expect(clippy::unnecessary_wraps)]
pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
// SAFETY: getgid is always successful and has no preconditions.
Ok(vec![unsafe { libc::getgid() }])

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While these are straightforward, we prefer using the nix crate instead of direct unsafe libc calls. Could you please update the libc calls in this file to go through it instead?


#[expect(clippy::unnecessary_wraps)]
pub(crate) fn get_all_groups() -> Result<Vec<String>, error::Error> {
Ok(vec!["mobile".to_owned()])

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you find a case where it's important to return at least one group instead of just returning an empty vector?


#[expect(clippy::unnecessary_wraps)]
pub(crate) fn get_current_username() -> Result<String, error::Error> {
Ok(std::env::var("USER").unwrap_or_else(|_| "mobile".to_owned()))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is USER typically set in the iOS targets you've tested with? If so, I'm tempted to avoid a hard-coded fake fallback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants