One ./build.sh entry point for Flutter, Tauri, Xcode iOS, Android/Kotlin/Gradle, React, Next.js, and Node — shared by humans, CI, AI agents, and MCP.
Quick start · Architecture · Commands · Security · Troubleshooting
Universal Build Script decides whether the current directory is one project or a monorepo, detects buildable projects, removes nested duplicates, and dispatches each project to an ecosystem adapter.
In 3.3, Python infers Node workspace, Flutter path, Gradle composite, and explicit project dependencies, then schedules topological layers. It also owns the Xcode-only iOS adapter. A dependency-free local stdio MCP server provides root-scoped read-only tools by default.
| Concern | Default behavior |
|---|---|
| Interaction | Non-interactive and CI-safe |
| Version | Unchanged unless a bump is explicitly requested |
| Monorepo | Build detected projects in topological dependency order |
| Flutter | macOS: AAB + IPA; other hosts: AAB |
| Tauri | Native OS bundle; signed .pkg when macOS signing is complete |
| Failure | Continue independent projects, then return an aggregate failure |
| Runtime update | Never performed during a normal build |
Install into an application root or monorepo root:
curl -fsSL https://raw.githubusercontent.com/Loop-Suite/Universal-Build-Script/main/install.sh | bashPython 3.9 or newer is required. Rust is optional:
curl -fsSL https://raw.githubusercontent.com/Loop-Suite/Universal-Build-Script/main/install.sh \
| UBS_BUILD_RUST_HELPER=true bash
./scripts/build-rust-helper.shUpgrading from 2.x to 3.x: run the installer once with
UBS_FORCE=true. After 3.x is installed,./build.sh updatemanages the complete 25-file runtime bundle.
The transactional installer defaults to the immutable current release ref. UBS_INSTALL_REF selects another immutable ref. Key controls are UBS_JOBS, UBS_INSTALL_MODE, UBS_MANAGE_GITIGNORE, UBS_GRADLE_FLAGS, and UBS_GRADLE_OPTIMIZE.
Inspect first, then build:
./build.sh detect
./build.sh audit
./build.sh plan --json
./build.sh graph --json
./build.shBuild selected outputs and write a machine-readable result:
./build.sh \
--flutter-outputs appbundle,web \
--version-bump none \
--report-json .ubs/build-report.jsonflowchart TD
U["Human / CI / AI / MCP"] --> B["build.sh compatibility entry point"]
B --> D["Detect and deduplicate projects"]
D --> P["Infer dependencies and create topological plan"]
P --> A{"Adapter"}
A -->|"Tauri"| T["OS bundle / signed macOS package"]
A -->|"Flutter"| F["AAB / APK / IPA / Web"]
A -->|"Gradle"| G["Android / Kotlin / KMP tasks"]
A -->|"Node"| N["React / Next / Node build script"]
A -->|"Xcode"| X["Release archive / optional IPA export"]
T --> R["Exit status + artifact report"]
F --> R
G --> R
N --> R
X --> R
flowchart LR
R["Directory"] --> T{"Tauri config?"}
T -->|"yes"| TA["Register Tauri; suppress nested frontend"]
T -->|"no"| F{"Flutter pubspec?"}
F -->|"yes"| FL["Register Flutter; suppress platform folders"]
F -->|"no"| X{"Xcode workspace/project?"}
X -->|"yes"| XI["Classify Xcode-only iOS"]
X -->|"no"| G{"Gradle settings?"}
G -->|"yes"| GR["Classify Android / KMP / Kotlin / Gradle"]
G -->|"no"| N{"package.json with scripts.build?"}
N -->|"yes"| NO["Classify Next / React / Node"]
N -->|"no"| S["Scan child directories"]
Precedence is Tauri → Flutter → Xcode → Gradle → Node. This prevents nested Tauri and Flutter platform projects from being duplicated.
sequenceDiagram
participant C as Caller
participant B as build.sh
participant A as Adapter
participant E as Ecosystem CLI
participant J as JSON report
C->>B: build --report-json report.json
B->>B: detect, filter, validate
loop each selected project
B->>A: run in project directory
A->>E: release build
E-->>A: status and outputs
A-->>B: exit code
B->>J: type, project, status, artifacts
end
B-->>C: aggregate exit status
flowchart TB
E["Thin ./build.sh entry point"] --> P["Python 3: options, discovery, planning, scheduling, Node, Gradle"]
P --> S["Bash: Flutter, Tauri, install, recovery"]
P --> N["Node package manager"]
P --> G["Gradle Wrapper / Gradle"]
S --> F["Flutter CLI"]
S --> T["Tauri/Cargo + Apple tooling"]
P --> R["Rust: batch manifest hashing and verification"]
P --> J["Machine-readable contract"]
F --> A["Artifacts"]
T --> A
G --> A
J --> C["AI / MCP / CI"]
A --> C
The project is intentionally not shell-only. Use --jobs N for bounded conflict-group concurrency. Node installs default to UBS_INSTALL_MODE=auto, which hashes workspace package/lock/config/patch and runtime inputs; use always to force it. Rust batch verification avoids one helper process per managed file, with a portable fallback when the helper is absent.
stateDiagram-v2
[*] --> Detect
Detect --> Select
Select --> Build
Build --> Success: exit 0
Build --> Failed: non-zero
Success --> More
Failed --> Stop: fail-fast
Failed --> More: default
More --> Build: next project
More --> Aggregate: no projects left
Stop --> Aggregate
Aggregate --> [*]
| Type | Detection | Default action | Typical output |
|---|---|---|---|
| Tauri 2 | src-tauri/tauri.conf.json |
package manager tauri build |
native OS bundle; optional macOS .pkg |
| Flutter | Flutter SDK in pubspec.yaml |
selected release outputs | AAB, split APK, IPA, Web, symbols |
| Xcode iOS | root *.xcworkspace or *.xcodeproj |
Release archive; optional export | XCArchive, IPA |
| Android | Android Gradle plugin | app bundleRelease; otherwise build |
project-defined Gradle outputs |
| Kotlin Multiplatform | KMP Gradle plugin | build |
target-specific outputs |
| Kotlin/JVM or Gradle | Gradle configuration | build |
JAR or project-defined outputs |
| Next.js / React / Node | string scripts.build |
package manager build script | .next, dist, build, or project-defined |
Generated and dependency directories such as .git, node_modules, build, dist, target, .gradle, .dart_tool, and .next are excluded from recursive discovery.
# Read-only discovery and configuration review
./build.sh detect --json /workspace
./build.sh audit --json /workspace
./build.sh plan --json /workspace
./build.sh graph --json /workspace
# Build one project or filtered monorepo projects
./build.sh build --project apps/mobile
./build.sh build --all --type flutter
# Explicit Flutter deliverables
./build.sh --flutter-outputs appbundle,apk,ipa,web
# Stop after the first project failure
./build.sh --fail-fast
# Structured build result
./build.sh --report-json .ubs/build-report.jsonImportant options:
| Option | Meaning |
|---|---|
| `--version-bump none | build |
| `--flutter-outputs auto | LIST` |
| `--flutter-platform auto | all |
--project PATH |
Build one target plus its detected prerequisite projects |
--all --type TYPE |
Filter monorepo projects |
--clean / --skip-clean |
Flutter cache policy |
--report-json PATH |
Write per-project status and discovered artifacts |
Exit code 0 means every selected project succeeded, 1 means discovery/build failure or no matching project, and 2 means invalid arguments.
After all selected projects finish, an interactive local terminal opens each successful output location with Finder, Explorer, or xdg-open. Flutter collapses outputs to build/; Tauri opens bundle or signed-package locations; Gradle opens output/library folders; Node opens dist, build, or .next; and Xcode opens build/ubs.
flowchart LR
B["Build selected projects"] --> S{"Successful artifacts?"}
S -->|"no"| E["Print status only"]
S -->|"yes"| M{"UBS_OPEN_OUTPUT"}
M -->|"auto + local TTY"| D["Normalize output folders"]
M -->|"true"| D
M -->|"false / CI / pipe"| E
D --> O["Finder / Explorer / xdg-open"]
UBS_OPEN_OUTPUT=auto is the default and never opens a GUI implicitly in CI or a non-interactive pipe. Use UBS_OPEN_OUTPUT=true to force it or UBS_OPEN_OUTPUT=false to disable it. UBS_NO_OPEN=true remains a compatibility off switch. Opening is best effort and cannot turn a successful build into a failed build; UBS_NO_NOTIFY controls macOS notifications separately.
flowchart TD
S["Output selection"] --> A{"auto?"}
A -->|"macOS"| MI["AAB + IPA"]
A -->|"other host"| AN["AAB"]
A -->|"explicit list"| E["Any AAB / APK / IPA / Web combination"]
MI --> I{"IPA requested?"}
E --> I
I -->|"yes"| P{"Project ExportOptions.plist exists?"}
P -->|"yes"| O["Use project policy"]
P -->|"no"| T["Use managed generic App Store template"]
O --> B["Release build + separated symbols"]
T --> B
AN --> B
Native Flutter outputs use release mode, obfuscation, and split debug information. Web uses release optimization and tree shaking, but native Dart obfuscation does not apply to Web.
flowchart TD
B["tauri build"] --> O{"Host OS"}
O -->|"Windows"| W["Project-configured MSI / NSIS bundle"]
O -->|"Linux"| L["Project-configured deb / AppImage / rpm bundle"]
O -->|"macOS"| M{"Package mode"}
M -->|"unsigned"| A["Default .app"]
M -->|"auto"| C{"All Apple signing inputs available?"}
M -->|"signed"| V{"All Apple signing inputs available?"}
C -->|"no"| A
C -->|"yes"| P["codesign verification + signed .pkg"]
V -->|"yes"| P
V -->|"no"| E["Fail before packaging"]
Package manager selection uses packageManager, then lock files for pnpm, Yarn, Bun, and finally npm. Frozen/immutable installation is used where supported.
./build.sh graph --json returns nodes, dependency edges, and topological layers. Builds use the same layers: independent projects in one layer may run under --jobs N, while a failed layer blocks its dependents. UBS infers Node workspace package references, Flutter path: dependencies, and Gradle includeBuild(...). Add relationships that cannot be inferred in ubs.dependencies.json:
{"schema_version":1,"dependencies":{"apps/web":["packages/ui"]}}Paths must remain inside the workspace; cycles are rejected. Native roots containing *.xcworkspace or *.xcodeproj are detected as ios-xcode. On macOS, UBS archives a shared scheme in Release mode. Set UBS_XCODE_SCHEME when multiple schemes are ambiguous, and set UBS_XCODE_EXPORT=true plus UBS_XCODE_EXPORT_OPTIONS to export an IPA.
A normal build never downloads UBS code. Update operations are explicit:
./build.sh update --check
./build.sh update --dry-run
./build.sh update
./build.sh update --check --json
./build.sh update --prune-backups 30sequenceDiagram
participant C as Caller
participant U as Updater
participant H as HTTPS source
participant B as Backup
participant F as Managed files
C->>U: check / dry-run / apply
U->>H: fetch manifest
U->>U: validate SemVer, allowlist, duplicates, missing paths
U->>F: compare local SHA-256
alt check or dry-run
U-->>C: changed paths only
else apply
U->>H: download every changed file
U->>U: verify every SHA-256 before mutation
U->>B: persist previous files
U->>F: temporary file + rename
alt partial failure
B-->>F: roll back installed files
end
U-->>C: version and backup path
end
The manifest is protected by an ECDSA (P-256/SHA-256) signature: install.sh and scripts/lib/update.sh carry the public key and reject the manifest outright if scripts/update-manifest.txt.sig is missing or invalid, even when every file checksum matches. The private signing key never leaves the release maintainer's local machine (never stored as a CI secret) — release: scripts/generate-update-manifest.sh > scripts/update-manifest.txt && scripts/sign-update-manifest.sh. The updater also blocks path traversal, symbolic-link destinations, concurrent writers, and downgrades unless explicitly permitted.
.gitignore excludes real environment files, Apple/Android signing material, service configuration, caches, and generated packages. Example files must contain placeholders only.
git status --short
git check-ignore .env .env.macos signing/App.provisionprofile build/app.aabIgnore rules do not erase tracked files or commit author metadata. Rotate any exposed credential before considering history rewriting.
flowchart LR
D["Developer machine"] --> E["Placeholder examples"]
D --> S["Secrets and signing inputs"]
D --> O["Generated artifacts"]
E -->|"tracked"| G["Git / PR"]
S -->|"ignored"| L["Local or secret store"]
O -->|"ignored"| L
G --> C["CI privacy validation"]
S -.->|"injected at build time"| R["Build runtime"]
The repository includes skills/universal-build. An agent should follow:
detect --json → audit --json → plan --json → explicit user approval → build --report-json
Run the bundled dependency-free stdio server with python3 /ABSOLUTE/PATH/scripts/ubs_mcp.py and set UBS_MCP_ROOT to the allowed workspace. It exposes ubs_detect, ubs_audit, ubs_plan, ubs_graph, and ubs_update_check. ubs_build is hidden unless UBS_MCP_ALLOW_BUILD=true; a real build also requires confirm=true. Paths outside the configured root and arbitrary shell fragments are rejected.
audit is static evidence, not release certification. It distinguishes release optimization, deliberate obfuscation, symbol separation, and signing. Verify actual outputs with ecosystem tools when high assurance is required; preserve Flutter symbols, Android mappings, native symbols, and required source maps.
bash -n build.sh install.sh scripts/*.sh scripts/lib/*.sh tests/*.sh
python tests/test_python_core.py
python tests/test_mcp.py
bash tests/test-detection.sh
bash tests/test-install.sh
bash tests/test-python-adapters.sh
bash tests/test-update.sh
bash tests/test-rust-helper.sh
scripts/generate-update-manifest.sh > /tmp/update-manifest.txt
diff -u scripts/update-manifest.txt /tmp/update-manifest.txtTests use temporary fixtures and mocked ecosystem commands. Real SDK builds, Apple/Android signing, and artifact-level reverse engineering remain project-specific validation steps.
- Not detected: confirm the framework marker and run
./build.sh detect. - macOS auto mode unexpectedly builds iOS: use
--flutter-platform androidor explicit outputs. - Android flavor: set
UBS_GRADLE_TASK=:app:bundleProdRelease. - Tauri creates
.appinstead of.pkg: run withUBS_TAURI_PACKAGE_MODE=signedto expose missing signing inputs. - Wrong package manager: align
packageManagerand the committed lock file.
- Builds are sequential by default.
--jobs Nruns non-overlapping projects within each topological layer; shared Node workspaces and overlapping paths stay serialized. - Automatic dependency inference covers Node package names, Flutter paths, and Gradle composites. Generated-code or custom-task relationships need
ubs.dependencies.json. - Ambiguous Xcode schemes require
UBS_XCODE_SCHEME. - Gradle flavors, custom release tasks, and KMP deployment tasks may require overrides.
- Tauri JS obfuscation assumes a
dist/frontend output. - Artifact reporting searches known default output locations.
- Automatic folder opening uses the same artifact rules, so custom output paths may need manual navigation.
- The update manifest supports external hash pinning but not an independent signature/transparency log.
MIT License — Copyright © 2026 kimdzhekhon