From 1a3e9562d3a1f69974cfd33a2ba569baa96b5e7f Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+savagemanage@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:03:59 +0000 Subject: [PATCH 1/3] feat: one redrob-board binary for the command line; drop the web build - board-cli, board-drc, board-kicad-export and board-gerber-compare become subcommands of redrob-board (crates/app/src/cli): pipeline, tool, examples, drc, kicad-export, gerber-compare. Exit codes are unchanged. On Windows the command line attaches to the console it was started from. - the browser build is gone: apps/redrob-board-web, packaging/web, the wasm CI step, the release web job and the wasm-release profile. - board-fixtures and board-sim-fixtures stay as developer tools in their crates. --- .github/workflows/ci.yml | 4 - .github/workflows/release.yml | 38 +--------- Cargo.lock | 22 +++--- Cargo.toml | 7 +- README.ko.md | 9 +-- README.md | 13 ++-- apps/redrob-board-web/Cargo.toml | 27 ------- apps/redrob-board-web/Trunk.toml | 10 --- apps/redrob-board-web/index.html | 21 ------ apps/redrob-board-web/src/main.rs | 31 -------- crates/agent/Cargo.toml | 3 - crates/app/Cargo.toml | 14 ++-- .../{drc/src/main.rs => app/src/cli/drc.rs} | 9 +-- .../src/main.rs => app/src/cli/gerber.rs} | 9 +-- .../src/main.rs => app/src/cli/kicad.rs} | 9 +-- crates/app/src/cli/mod.rs | 51 +++++++++++++ .../src/main.rs => app/src/cli/pipeline.rs} | 13 ++-- crates/app/src/lib.rs | 2 +- crates/app/src/main.rs | 22 +++++- crates/gerber/Cargo.toml | 3 - crates/kicad/Cargo.toml | 3 - docs/ROADMAP.ko.md | 2 +- packaging/web/.htaccess | 16 ---- packaging/web/README.md | 74 ------------------- packaging/web/_headers | 13 ---- packaging/web/package.sh | 56 -------------- 26 files changed, 120 insertions(+), 361 deletions(-) delete mode 100644 apps/redrob-board-web/Cargo.toml delete mode 100644 apps/redrob-board-web/Trunk.toml delete mode 100644 apps/redrob-board-web/index.html delete mode 100644 apps/redrob-board-web/src/main.rs rename crates/{drc/src/main.rs => app/src/cli/drc.rs} (82%) rename crates/{gerber/src/main.rs => app/src/cli/gerber.rs} (91%) rename crates/{kicad/src/main.rs => app/src/cli/kicad.rs} (84%) create mode 100644 crates/app/src/cli/mod.rs rename crates/{agent/src/main.rs => app/src/cli/pipeline.rs} (88%) delete mode 100644 packaging/web/.htaccess delete mode 100644 packaging/web/README.md delete mode 100644 packaging/web/_headers delete mode 100755 packaging/web/package.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5a3267..7290a98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,6 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt - targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 - name: Desktop build deps and a software Vulkan driver for the headless UI snapshot if: runner.os == 'Linux' @@ -44,6 +43,3 @@ jobs: run: cargo clippy --workspace --all-targets --locked -- -D warnings - name: Test run: cargo test --workspace --locked - - name: Browser build compiles - if: runner.os == 'Linux' - run: cargo check -p redrob-board-web --target wasm32-unknown-unknown --locked diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d7f16b8..30ba098 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -254,45 +254,9 @@ jobs: path: dist/release/* if-no-files-found: error - web: - name: Web (wasm) - needs: version - runs-on: ubuntu-latest - # No `environment: release`: this job signs nothing, so it gets no release secrets. - env: - REDROB_BOARD_VERSION: ${{ needs.version.outputs.version }} - REDROB_BOARD_BUILD_DATE: ${{ needs.version.outputs.date }} - REDROB_BOARD_BUILD_SHA: ${{ needs.version.outputs.sha }} - TRUNK_VERSION: 0.21.14 - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.version.outputs.sha }} - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown - - uses: Swatinem/rust-cache@v2 - with: - key: release-web - - name: Install trunk - run: | - curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" \ - | tar -xz -C "$HOME/.cargo/bin" trunk - trunk --version - - name: Apply version override - if: inputs.version != '' - run: cargo xtask version set "$REDROB_BOARD_VERSION" - - name: Build and zip - run: packaging/web/package.sh - - uses: actions/upload-artifact@v4 - with: - name: release-web - path: dist/release/* - if-no-files-found: error - release: name: Publish GitHub Release - needs: [version, macos, windows, linux, web] + needs: [version, macos, windows, linux] # Only a version tag publishes; a manual dispatch stops before this job. if: github.ref_type == 'tag' runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 4917cdc..ca32aed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2819,6 +2819,8 @@ dependencies = [ "board-designs", "board-drc", "board-geom", + "board-gerber", + "board-kicad", "board-layout", "board-netlist", "board-place", @@ -2828,19 +2830,7 @@ dependencies = [ "egui_kittest", "serde_json", "winresource", -] - -[[package]] -name = "redrob-board-web" -version = "0.2.0" -dependencies = [ - "eframe", - "egui", - "log", - "redrob-board", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", + "winsafe", ] [[package]] @@ -4255,6 +4245,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "winsafe" +version = "0.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef0ffc427f045c0cc9ebffd6f4f91153dcd2a1547b5c3c29ee3f541e16e95c6" + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/Cargo.toml b/Cargo.toml index 18c48b0..9dbfee2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["crates/*", "apps/*", "xtask"] +members = ["crates/*", "xtask"] [workspace.package] version = "0.2.0" @@ -54,8 +54,3 @@ opt-level = 1 lto = "thin" codegen-units = 1 -[profile.wasm-release] -inherits = "release" -opt-level = "s" -lto = "fat" -strip = "debuginfo" diff --git a/README.ko.md b/README.ko.md index 5592f3d..c9b7bb9 100644 --- a/README.ko.md +++ b/README.ko.md @@ -32,7 +32,7 @@ JavaScript 없음, 웹뷰 없음, 외부 EDA 바이너리 없음. 상류 도구( ```sh cargo test --workspace -cargo run -p board-agent --bin board-cli -- pipeline --design sensor-node --outline 30x25 --edge J1=left --edge SW1=right --out fab +cargo run -p redrob-board -- pipeline --design sensor-node --outline 30x25 --edge J1=left --edge SW1=right --out fab cargo run -p redrob-board # 데스크톱 앱 ``` @@ -43,7 +43,7 @@ cargo run -p redrob-board # 데스크톱 앱 일치. 배치 엔진은 14부품 센서 노드를 겹침 없이, 손 배치보다 배선 길이 54% 짧게 놓습니다(게이트 20%). 라우터는 분배기와 센서 노드 두 배치를 2층으로 끝까지 배선해 DRC 위반 0이고, KiCad도 배선된 센서 노드를 깨끗하다고 판정합니다. 우리 거버(동박·마스크·페이스트·외곽)와 드릴 파일은 같은 보드를 KiCad가 그린 것과 우리 리더로 10um 래스터화해 비교합니다: 동박·마스크·페이스트(모듈 접지판의 페이스트 창 격자 포함)·외곽·드릴 48개 파일 전부 픽셀 일치, 실크(자체 선 폰트 참조기호)는 느슨한 일치(위치·좌우·잉크량 2배 이내). BOM·픽앤플레이스 -CSV는 `board-bom`이 냅니다. `board-cli pipeline --design sensor-node --outline 30x25 --edge J1=left --edge SW1=right --out fab`이 +CSV는 `board-bom`이 냅니다. `redrob-board pipeline --design sensor-node --outline 30x25 --edge J1=left --edge SW1=right --out fab`이 전체 흐름을 돌려 배선 완주·DRC 0일 때만 종료 코드 0을 냅니다. `redrob-board`(egui) 앱은 같은 일을 버튼으로 하고, 헤드리스 `egui_kittest` 테스트가 그 버튼들을 눌러 렌더 결과를 `crates/app/tests/snapshots/sensor_node_routed.png`와 비교합니다. 앱의 AGENT 팔레트는 공유 Redrob Code 엔진(`redrob-engine-client`)과 대화하며, 호출자 소유 도구 10개(`board-agent`: @@ -57,7 +57,7 @@ catalog·write_schematic·place·route·drc·export_zip 등)를 선언합니다: 차동쌍, 길이 매칭 그룹, 내층 GND·전원 플레인이 있는 4층 보드(다른 넷 주변 존 채움, 섬·조각 제거; 내층 거버(G36/G37 영역)가 KiCad 플롯과 픽셀 일치, KiCad DRC 0), 자체 스트로크 폰트 실크와 모듈 플레이트 페이스트 창, `board-sim`(넷리스트의 선형 DC 동작점·AC 소신호, `oracle` 워크플로 `sim` 잡에서 ngspice와 1% 이내 일치, `simulate` 도구와 레일 검사), 3D 미리보기(코트야드 위 상자 부품, 드래그 -회전), 브라우저 빌드, 보드 하우스 패키지: `export_zip`이 JLCPCB·PCBWay 리더가 기대하는 이름으로 파일을 쓰고 주문 시트(층수·크기·규칙·LCSC +회전), 보드 하우스 패키지: `export_zip`이 JLCPCB·PCBWay 리더가 기대하는 이름으로 파일을 쓰고 주문 시트(층수·크기·규칙·LCSC 재고 커버·업로드 위치)를 아카이브에 넣습니다. 앱의 ORDER 팔레트는 Labs 스위치(`REDROB_BOARD_LABS=order` 또는 Labs 체크박스)이고 꺼져 있으면 그리지 않습니다. 주문 버튼은 없습니다: 두 하우스 모두 판매자 계정 없는 공개 주문 API가 없어서, 패키지와 시트가 인계점입니다. @@ -67,8 +67,7 @@ KiCad 플롯과 픽셀 일치, KiCad DRC 0), 자체 스트로크 폰트 실크 Linux AppImage/deb/rpm/tar.gz(x86_64·aarch64)가 있습니다: [github.com/redrob-labs/redrob-board/releases](https://github.com/redrob-labs/redrob-board/releases). `vX.Y.Z` 태그가 `.github/workflows/release.yml`을 돌려 한 커밋에서 모든 잡을 빌드하고 릴리즈를 발행합니다. -패키징 스크립트는 `packaging/`에 있습니다. 릴리즈에는 브라우저 빌드(`redrob-board-web-<버전>.zip`, 정적 WebAssembly 사이트, -호스팅 안내는 `packaging/web/README.md`)도 들어갑니다: 엔진 없는 같은 창이라 AGENT 팔레트가 그 사실을 말합니다. +패키징 스크립트는 `packaging/`에 있습니다. ## 라이선스 diff --git a/README.md b/README.md index 927a36d..a44d208 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,13 @@ called done. Nothing of theirs ships in the product. See [UPSTREAM.md](./UPSTREA ```sh cargo test --workspace -cargo run -p board-agent --bin board-cli -- pipeline --design sensor-node --outline 30x25 --edge J1=left --edge SW1=right --out fab +cargo run -p redrob-board -- pipeline --design sensor-node --outline 30x25 --edge J1=left --edge SW1=right --out fab cargo run -p redrob-board # the desktop app ``` +The app binary is also the command line: `redrob-board pipeline | tool | examples | drc | +kicad-export | gerber-compare` (`redrob-board help`). Each exit code is the verdict. + ## Status Stages 0 (bootstrap), 1 (footprint library, DRC), 2 (placement), 3 (routing), 4 (manufacturing @@ -52,7 +55,7 @@ divider and both sensor-node placements on two layers with zero DRC violations, the routed sensor node is clean. Our Gerbers (copper, mask, paste, edge cuts) and Excellon drill files are compared with KiCad's plots of the same boards by rasterising both at 10 um with our own reader: 48 of 48 copper, mask, paste (with the grid of paste windows on the module ground plate), edge and drill files are pixel-identical, and the silkscreen (reference designators in our own stroke font) matches KiCad's loosely -- same place, same mirror, same ink within a factor of two. BOM and pick-and-place CSVs come from `board-bom`. -`board-cli pipeline --design sensor-node --outline 30x25 --edge J1=left --edge SW1=right --out fab` +`redrob-board pipeline --design sensor-node --outline 30x25 --edge J1=left --edge SW1=right --out fab` runs the whole flow and exits 0 only when routing is complete and DRC is clean; `redrob-board` (egui) does the same from buttons, and a headless `egui_kittest` test drives those buttons and compares the rendered frame to `crates/app/tests/snapshots/sensor_node_routed.png`. The app's @@ -72,7 +75,7 @@ nets, islands and slivers dropped; our inner-layer Gerbers with G36/G37 regions to KiCad's plots and KiCad's DRC stays at zero), silkscreen in our own stroke font and paste windows on module plates, `board-sim` (linear DC operating point and AC small-signal of the netlist, checked against ngspice within 1% in the `oracle` workflow's `sim` job, with a `simulate` tool and rail -checks), a 3D preview (parts as boxes over their courtyards, drag to turn), a browser build, and +checks), a 3D preview (parts as boxes over their courtyards, drag to turn), and board-house packages: `export_zip` names the files the way JLCPCB or PCBWay's reader expects and puts an order sheet (layers, size, rules, LCSC stock coverage, where to upload) in the archive. The app's ORDER palette is a Labs switch (`REDROB_BOARD_LABS=order` or the Labs checkbox) and is not @@ -86,9 +89,7 @@ Releases carry a signed and notarized macOS disk image (universal), signed Windo and portable zips (x64, arm64) and Linux AppImage/deb/rpm/tar.gz (x86_64, aarch64): [github.com/redrob-labs/redrob-board/releases](https://github.com/redrob-labs/redrob-board/releases). A `vX.Y.Z` tag runs `.github/workflows/release.yml`, which builds every job from one commit and -publishes the release; the packaging scripts live in `packaging/`. The release also carries a -browser build (`redrob-board-web-.zip`, a static WebAssembly site; `packaging/web/README.md` -has the hosting notes): the same window without the engine, so the AGENT palette says so. +publishes the release; the packaging scripts live in `packaging/`. ## License diff --git a/apps/redrob-board-web/Cargo.toml b/apps/redrob-board-web/Cargo.toml deleted file mode 100644 index fc2e187..0000000 --- a/apps/redrob-board-web/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "redrob-board-web" -description = "Redrob Board in the browser: place, route, check and preview a board without installing anything" -version.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true -publish = false - -[lints] -workspace = true - -[[bin]] -name = "redrob-board-web" -path = "src/main.rs" - -[dependencies] -# No `agent` feature: the browser has no engine to talk to, and the palette says so. -redrob-board = { workspace = true, default-features = false } -egui = { workspace = true } -log = "0.4" - -[target.'cfg(target_arch = "wasm32")'.dependencies] -eframe = { workspace = true, features = ["default_fonts", "glow"] } -wasm-bindgen = "0.2" -wasm-bindgen-futures = "0.4" -web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlCanvasElement"] } diff --git a/apps/redrob-board-web/Trunk.toml b/apps/redrob-board-web/Trunk.toml deleted file mode 100644 index e2ed351..0000000 --- a/apps/redrob-board-web/Trunk.toml +++ /dev/null @@ -1,10 +0,0 @@ -# Build with: trunk build --release --cargo-profile wasm-release (from apps/redrob-board-web) -[build] -target = "index.html" -dist = "../../dist/web" -public_url = "./" -filehash = true - -[serve] -addresses = ["127.0.0.1"] -port = 8767 diff --git a/apps/redrob-board-web/index.html b/apps/redrob-board-web/index.html deleted file mode 100644 index 114a8c3..0000000 --- a/apps/redrob-board-web/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - Redrob Board - - - - - - -
Loading Redrob Board…
- - diff --git a/apps/redrob-board-web/src/main.rs b/apps/redrob-board-web/src/main.rs deleted file mode 100644 index bfcdff0..0000000 --- a/apps/redrob-board-web/src/main.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Redrob Board in the browser: the same window as the desktop app, without the engine (the -//! AGENT palette says so). Everything else -- examples, placement, routing, DRC, 3D -- runs in -//! WebAssembly; the fabrication files can be computed but not written to disk here. - -#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] - -#[cfg(target_arch = "wasm32")] -fn main() { - use wasm_bindgen::JsCast; - eframe::WebLogger::init(log::LevelFilter::Warn).ok(); - wasm_bindgen_futures::spawn_local(async { - let Some(document) = web_sys::window().and_then(|w| w.document()) else { return }; - let Some(canvas) = document.get_element_by_id("board_canvas").and_then(|c| c.dyn_into::().ok()) else { - log::error!("no canvas"); - return; - }; - let result = - eframe::WebRunner::new().start(canvas, eframe::WebOptions::default(), Box::new(|_cc| Ok(Box::new(board_app::BoardApp::default())))).await; - if let Some(loading) = document.get_element_by_id("board_loading") { - loading.remove(); - } - if let Err(e) = result { - log::error!("couldn't start: {e:?}"); - } - }); -} - -#[cfg(not(target_arch = "wasm32"))] -fn main() { - eprintln!("redrob-board-web is the browser build: run `trunk serve` in apps/redrob-board-web."); -} diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index a512d7a..786d61d 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -27,6 +27,3 @@ serde_json = { workspace = true } [lints] workspace = true -[[bin]] -name = "board-cli" -path = "src/main.rs" diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 2549a0a..347c1f2 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -21,17 +21,19 @@ board-agent = { workspace = true } board-chat = { workspace = true, optional = true } serde_json = { workspace = true } egui = { workspace = true } +eframe = { workspace = true, features = ["default_fonts", "glow", "x11", "wayland"] } +# The command line (src/cli): `redrob-board pipeline|tool|examples|drc|kicad-export|gerber-compare`. +board-kicad = { workspace = true } +board-gerber = { workspace = true } [features] -# The AGENT palette (engine connection over the network); off in the browser build. +# The AGENT palette (engine connection over the network). default = ["agent"] agent = ["board-chat"] -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -eframe = { workspace = true, features = ["default_fonts", "glow", "x11", "wayland"] } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -eframe = { workspace = true, features = ["default_fonts", "glow"] } +# The command line attaches to the console it was started from (src/cli/mod.rs). +[target.'cfg(windows)'.dependencies] +winsafe = { version = "0.0.29", default-features = false, features = ["kernel"] } [dev-dependencies] egui_kittest = { workspace = true, features = ["wgpu", "snapshot", "eframe"] } diff --git a/crates/drc/src/main.rs b/crates/app/src/cli/drc.rs similarity index 82% rename from crates/drc/src/main.rs rename to crates/app/src/cli/drc.rs index b283129..2c6a8b4 100644 --- a/crates/drc/src/main.rs +++ b/crates/app/src/cli/drc.rs @@ -1,4 +1,4 @@ -//! `board-drc [rules.json]` -- exit 0 when clean, 1 when any rule is +//! `redrob-board drc [rules.json]` -- exit 0 when clean, 1 when any rule is //! violated, 2 on a usage or read error. The exit code is the gate; the text is for humans. use std::process::ExitCode; @@ -7,10 +7,9 @@ use board_drc::{Rules, check, summary}; use board_layout::Board; use board_netlist::Netlist; -fn main() -> ExitCode { - let args: Vec = std::env::args().skip(1).collect(); +pub fn main(args: &[String]) -> ExitCode { if args.len() < 2 || args.len() > 3 { - eprintln!("usage: board-drc [rules.json]"); + eprintln!("usage: redrob-board drc [rules.json]"); return ExitCode::from(2); } let read = |path: &str| std::fs::read_to_string(path).map_err(|e| format!("{path}: {e}")); @@ -25,7 +24,7 @@ fn main() -> ExitCode { }; match run() { Err(e) => { - eprintln!("board-drc: {e}"); + eprintln!("redrob-board drc: {e}"); ExitCode::from(2) } Ok(violations) if violations.is_empty() => { diff --git a/crates/gerber/src/main.rs b/crates/app/src/cli/gerber.rs similarity index 91% rename from crates/gerber/src/main.rs rename to crates/app/src/cli/gerber.rs index b5a28f4..1da66b1 100644 --- a/crates/gerber/src/main.rs +++ b/crates/app/src/cli/gerber.rs @@ -1,4 +1,4 @@ -//! `board-gerber-compare [pitch_um]` rasterises two Gerber files (or two Excellon +//! `redrob-board gerber-compare [pitch_um]` rasterises two Gerber files (or two Excellon //! files, when both end in `.drl`) and exits 0 only when they are pixel-for-pixel (hole-for-hole) //! identical. Both files are read with this crate's own reader; nothing else is trusted. //! @@ -12,10 +12,9 @@ use std::process::ExitCode; use board_gerber::{GerberFile, parse_drill}; use board_units::Nm; -fn main() -> ExitCode { - let args: Vec = std::env::args().skip(1).collect(); +pub fn main(args: &[String]) -> ExitCode { if args.len() < 2 || args.len() > 3 { - eprintln!("usage: board-gerber-compare [pitch_um]"); + eprintln!("usage: redrob-board gerber-compare [pitch_um]"); return ExitCode::from(2); } let read = |p: &str| std::fs::read_to_string(p).map_err(|e| format!("{p}: {e}")); @@ -65,7 +64,7 @@ fn main() -> ExitCode { ExitCode::from(1) } Err(e) => { - eprintln!("board-gerber-compare: {e}"); + eprintln!("redrob-board gerber-compare: {e}"); ExitCode::from(2) } } diff --git a/crates/kicad/src/main.rs b/crates/app/src/cli/kicad.rs similarity index 84% rename from crates/kicad/src/main.rs rename to crates/app/src/cli/kicad.rs index a3753a2..97ba9dd 100644 --- a/crates/kicad/src/main.rs +++ b/crates/app/src/cli/kicad.rs @@ -1,4 +1,4 @@ -//! `board-kicad-export [rules.json]` writes +//! `redrob-board kicad-export [rules.json]` writes //! `/board.kicad_pcb` and `/board.kicad_pro` for the oracle run. use std::process::ExitCode; @@ -8,10 +8,9 @@ use board_kicad::{export_pcb, export_pcb_with_fills, export_project}; use board_layout::Board; use board_netlist::Netlist; -fn main() -> ExitCode { - let args: Vec = std::env::args().skip(1).collect(); +pub fn main(args: &[String]) -> ExitCode { if args.len() < 3 || args.len() > 4 { - eprintln!("usage: board-kicad-export [rules.json]"); + eprintln!("usage: redrob-board kicad-export [rules.json]"); return ExitCode::from(2); } let run = || -> Result<(), String> { @@ -35,7 +34,7 @@ fn main() -> ExitCode { match run() { Ok(()) => ExitCode::SUCCESS, Err(e) => { - eprintln!("board-kicad-export: {e}"); + eprintln!("redrob-board kicad-export: {e}"); ExitCode::from(2) } } diff --git a/crates/app/src/cli/mod.rs b/crates/app/src/cli/mod.rs new file mode 100644 index 0000000..f59b2b0 --- /dev/null +++ b/crates/app/src/cli/mod.rs @@ -0,0 +1,51 @@ +//! The command line: `redrob-board ` works without opening the window. +//! +//! ```text +//! redrob-board pipeline --design NAME --outline WxH [--edge REF=left|right|top|bottom]... [--seed N] --out DIR +//! redrob-board tool '' one tool call, JSON result on stdout +//! redrob-board examples the example designs +//! redrob-board drc [rules.json] +//! redrob-board kicad-export [rules.json] +//! redrob-board gerber-compare [pitch_um] +//! ``` +//! +//! Each command's exit code is its verdict (0 pass, 1 fail, 2 usage or read error). + +mod drc; +mod gerber; +mod kicad; +mod pipeline; + +use std::process::ExitCode; + +const COMMANDS: [&str; 6] = ["pipeline", "tool", "examples", "drc", "kicad-export", "gerber-compare"]; + +/// Runs the command line when the first argument is a command; `None` opens the window. +pub fn dispatch(args: &[String]) -> Option { + let cmd = args.first()?.as_str(); + if !COMMANDS.contains(&cmd) && !matches!(cmd, "help" | "--help" | "-h") { + return None; + } + attach_console(); + Some(match cmd { + "pipeline" | "tool" | "examples" => pipeline::main(args), + "drc" => drc::main(&args[1..]), + "kicad-export" => kicad::main(&args[1..]), + "gerber-compare" => gerber::main(&args[1..]), + _ => { + println!("usage: redrob-board {}", COMMANDS.join(" | ")); + println!(" redrob-board [--version] (opens the window)"); + ExitCode::SUCCESS + } + }) +} + +/// The release build on Windows is a GUI program with no console of its own, so its output would +/// go nowhere; the command line writes to the console it was started from instead. +#[cfg(windows)] +fn attach_console() { + let _ = winsafe::AttachConsole(winsafe::PidParent::Parent); +} + +#[cfg(not(windows))] +fn attach_console() {} diff --git a/crates/agent/src/main.rs b/crates/app/src/cli/pipeline.rs similarity index 88% rename from crates/agent/src/main.rs rename to crates/app/src/cli/pipeline.rs index 1f007e2..eb1d9f5 100644 --- a/crates/agent/src/main.rs +++ b/crates/app/src/cli/pipeline.rs @@ -1,9 +1,9 @@ -//! `board-cli` -- the whole flow from the terminal, exit code = verdict. +//! `redrob-board pipeline | tool | examples` -- the whole flow from the terminal, exit code = verdict. //! //! ```text -//! board-cli pipeline --design sensor-node --outline 30x25 --edge J1=left --edge SW1=right --out DIR -//! board-cli tool '' # one tool call, JSON result on stdout -//! board-cli examples +//! redrob-board pipeline --design sensor-node --outline 30x25 --edge J1=left --edge SW1=right --out DIR +//! redrob-board tool '' # one tool call, JSON result on stdout +//! redrob-board examples //! ``` //! `pipeline` runs place -> route -> DRC -> export and exits 0 only when routing completed and //! DRC is clean. @@ -16,8 +16,7 @@ use board_designs::library; use board_drc::summary; use board_place::Edge; -fn main() -> ExitCode { - let args: Vec = std::env::args().skip(1).collect(); +pub fn main(args: &[String]) -> ExitCode { match args.first().map(String::as_str) { Some("examples") => { for n in example_names() { @@ -32,7 +31,7 @@ fn main() -> ExitCode { Some("pipeline") => pipeline(&args[1..]), _ => { eprintln!( - "usage: board-cli examples | tool '' | pipeline --design NAME --outline WxH [--edge REF=left|right|top|bottom]... [--seed N] --out DIR" + "usage: redrob-board examples | tool '' | pipeline --design NAME --outline WxH [--edge REF=left|right|top|bottom]... [--seed N] --out DIR" ); ExitCode::from(2) } diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index 2bdcdb1..c8d9160 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -524,7 +524,7 @@ impl BoardApp { fn connect_agent(&mut self) { if !self.chat.tried { self.chat.tried = true; - self.chat.unavailable = Some("The agent needs the desktop app: the browser build has no engine connection.".into()); + self.chat.unavailable = Some("The agent is not in this build (the `agent` feature is off).".into()); } } diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index c9decf5..96d4d3b 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -1,9 +1,25 @@ -//! Redrob Board desktop entry point. +//! Redrob Board desktop entry point; `redrob-board ` is the command line (`cli`). #![cfg_attr(windows, windows_subsystem = "windows")] -fn main() -> eframe::Result { - if std::env::args().skip(1).any(|a| a == "--version" || a == "-V") { +mod cli; + +fn main() -> std::process::ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + if let Some(code) = cli::dispatch(&args) { + return code; + } + match window(&args) { + Ok(()) => std::process::ExitCode::SUCCESS, + Err(e) => { + eprintln!("redrob-board: {e}"); + std::process::ExitCode::FAILURE + } + } +} + +fn window(args: &[String]) -> eframe::Result { + if args.iter().any(|a| a == "--version" || a == "-V") { println!("redrob-board {}", env!("CARGO_PKG_VERSION")); return Ok(()); } diff --git a/crates/gerber/Cargo.toml b/crates/gerber/Cargo.toml index cd0cb7b..ad8b8d4 100644 --- a/crates/gerber/Cargo.toml +++ b/crates/gerber/Cargo.toml @@ -16,6 +16,3 @@ thiserror = { workspace = true } [lints] workspace = true -[[bin]] -name = "board-gerber-compare" -path = "src/main.rs" diff --git a/crates/kicad/Cargo.toml b/crates/kicad/Cargo.toml index 67dc2f2..086ea35 100644 --- a/crates/kicad/Cargo.toml +++ b/crates/kicad/Cargo.toml @@ -23,6 +23,3 @@ board-schematic = { workspace = true } [lints] workspace = true -[[bin]] -name = "board-kicad-export" -path = "src/main.rs" diff --git a/docs/ROADMAP.ko.md b/docs/ROADMAP.ko.md index 0529f6f..5d30f3f 100644 --- a/docs/ROADMAP.ko.md +++ b/docs/ROADMAP.ko.md @@ -43,7 +43,7 @@ Redrob Silicon이 칩 안을, Redrob Board가 칩 사이를 맡는다. 둘은 | board-sim | SPICE급 회로 시뮬(선형 + 단순 비선형). 전원·바이어스 검증 | ngspice(CI 오라클) | | board-render | wgpu 레이어 뷰어, 3D 보드 미리보기 | — | | board-agent | 엔진 도구 정의: write_schematic, pick_parts, place, route, drc, export, order | redrob-engine-client | -| redrob-board (crates/app) | egui/eframe 앱. 웹 빌드는 wasm | cadcraft·launcher 구조 | +| redrob-board (crates/app) | egui/eframe 앱. 같은 실행 파일이 명령줄(`redrob-board pipeline` 등) | cadcraft·launcher 구조 | ## 단계 diff --git a/packaging/web/.htaccess b/packaging/web/.htaccess deleted file mode 100644 index 7b7e6e1..0000000 --- a/packaging/web/.htaccess +++ /dev/null @@ -1,16 +0,0 @@ -# Apache (see HOSTING.md). Needs mod_mime; mod_deflate and mod_headers are optional. -AddType application/wasm .wasm -AddType text/javascript .js - - - AddOutputFilterByType DEFLATE application/wasm text/javascript text/html - - - - - Header set Cache-Control "public, max-age=31536000, immutable" - - - Header set Cache-Control "no-cache" - - diff --git a/packaging/web/README.md b/packaging/web/README.md deleted file mode 100644 index 55c001b..0000000 --- a/packaging/web/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# Hosting Redrob Board for the web - -`redrob-board-web-.zip` (from the GitHub release, or `packaging/web/package.sh`) -holds a static site in `redrob-board-web-/`: - -| File | What it is | -|---|---| -| `index.html` | The page. It loads everything through relative URLs. | -| `redrob-board-web-.js` | wasm-bindgen glue (generated, ES module) | -| `redrob-board-web-_bg.wasm` | The app (egui, built with the `wasm-release` Cargo profile) | -| `_headers`, `.htaccess` | Sample header rules for Netlify/Cloudflare Pages and Apache | - -There is no server-side code and no hand-written JavaScript. Upload the folder's contents anywhere -that serves static files. - -## What the web version does - -In the browser there is no engine, so the AGENT palette says so; everything else -- the example -designs, placement, routing, the design rule check, the 3D preview -- runs in WebAssembly. The -fabrication files can be computed, but the browser build cannot write them to disk. - -## Any path works - -All URLs in `index.html` are relative (`public_url = "./"` in `apps/redrob-board-web/Trunk.toml`), -so the site works at a domain root (`https://example.com/`), under a prefix -(`https://example.com/apps/board/`) and from a CDN bucket. The asset names carry a content -hash, so they can be cached forever. Only `index.html` needs revalidation. - -`packaging/web/package.sh` fails if the wasm grows past 24 MiB (Cloudflare Pages and Workers -reject any single file over 25 MiB). - -## Required server settings - -- **MIME type:** serve `.wasm` as `application/wasm`. Browsers refuse to stream-compile it under - any other type. Serve `.js` as `text/javascript`. Most hosts already do both. For nginx, check - that `mime.types` has `application/wasm wasm;`. -- **Compression:** turn on gzip or Brotli for `.wasm`, `.js` and `.html`. -- **Caching:** `Cache-Control: public, max-age=31536000, immutable` on the hashed `.wasm` and - `.js` files, and `no-cache` on `index.html`. -- **Content Security Policy:** if you send one, allow `connect-src https://api.github.com` (and - `https://raw.githubusercontent.com` for the app icons) plus `'wasm-unsafe-eval'` in `script-src`. -- **No special isolation headers:** the app doesn't use `SharedArrayBuffer`, so it doesn't need - `Cross-Origin-Opener-Policy` or `Cross-Origin-Embedder-Policy`. - -nginx example: - -```nginx -location /board/ { - types { application/wasm wasm; text/javascript js; text/html html; } - gzip on; - gzip_types application/wasm text/javascript text/html; - location ~* \.(wasm|js)$ { add_header Cache-Control "public, max-age=31536000, immutable"; } - location ~* index\.html$ { add_header Cache-Control "no-cache"; } -} -``` - -Local test: `python3 -m http.server 8765` inside the folder, then open http://localhost:8765/. - -## Embedding in a page (iframe) - -```html - -``` - -- The app fills the iframe and follows its size, so size the iframe and not the app. -- **Sandboxed iframes** need at least `sandbox="allow-scripts allow-same-origin allow-popups"`; - `allow-popups` lets download links open. -- Don't send `X-Frame-Options: DENY` or a `frame-ancestors` CSP that excludes the embedding page. diff --git a/packaging/web/_headers b/packaging/web/_headers deleted file mode 100644 index fac0fbe..0000000 --- a/packaging/web/_headers +++ /dev/null @@ -1,13 +0,0 @@ -# Netlify / Cloudflare Pages header rules (see HOSTING.md). Other hosts: set the same headers. -/* - X-Content-Type-Options: nosniff - -/*.wasm - Content-Type: application/wasm - Cache-Control: public, max-age=31536000, immutable - -/*.js - Cache-Control: public, max-age=31536000, immutable - -/index.html - Cache-Control: no-cache diff --git a/packaging/web/package.sh b/packaging/web/package.sh deleted file mode 100755 index fdc04e2..0000000 --- a/packaging/web/package.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env bash -# Build the browser version and zip it: $DIST/redrob-board-web-.zip -# -# Usage: packaging/web/package.sh [--skip-build] -# -# Needs: trunk (brew install trunk / cargo install trunk --locked) and the wasm32-unknown-unknown -# target. The zip holds a self-contained static site in redrob-board-web-/ that works -# from any URL path and inside an