diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
@@ -47,3 +47,34 @@ jobs:
cargo run --quiet -- build -i docs -o "$RUNNER_TEMP/a"
cargo run --quiet -- build -i docs -o "$RUNNER_TEMP/b"
diff -r "$RUNNER_TEMP/a" "$RUNNER_TEMP/b"
+
+ # musl 릴리스 아티팩트가 쓰는 pure-rust 엔진 조합이 썩지 않게 지킨다.
+ # 크로스 엔진 바이트 비교는 하지 않는다 — fancy 덤프에는 문법 7개
+ # (PowerShell·JS(Babel)·Salt State·ARM Assembly 등)가 통째로 없어서 해당
+ # 펜스가 평문 폴백이 되고 js 토큰 해석도 갈린다. 아티팩트 간 차이는 버그가
+ # 아니라 문서화된 계약이다(docs/content/start/installation.md). 결정성은
+ # 엔진별로 지킨다.
+ pure-rust:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ components: clippy
+
+ - uses: Swatinem/rust-cache@v2
+ with:
+ key: pure-rust
+
+ - name: Clippy
+ run: cargo clippy --all-targets --no-default-features --features pure-rust -- -D warnings
+
+ - name: Test
+ run: cargo test --no-default-features --features pure-rust
+
+ - name: Build is reproducible
+ run: |
+ cargo run --quiet --no-default-features --features pure-rust -- build -i docs -o "$RUNNER_TEMP/a"
+ cargo run --quiet --no-default-features --features pure-rust -- build -i docs -o "$RUNNER_TEMP/b"
+ diff -r "$RUNNER_TEMP/a" "$RUNNER_TEMP/b"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
@@ -21,13 +21,18 @@ jobs:
fail-fast: false
matrix:
include:
- # musl 정적 링크. 이게 되는지가 이 프로젝트의 의존성 선택을 지배했다 —
- # syntect를 default-fancy(순수 Rust)로 고정한 이유가 Oniguruma의 C
- # 바인딩이 여기서 깨지기 때문이다. 이 잡이 그 결정의 검증이다.
+ # musl 정적 링크. 정적 아티팩트만 pure-rust 엔진으로 빌드한다 —
+ # Oniguruma(C 바인딩)가 정적 링크를 깨는 지점이 여기라서다. 기본
+ # 빌드는 onig(코드 무거운 코퍼스 실측 3배)지만, 이 잡의
+ # --no-default-features가 onig를 컴파일 타임에 차단한다. musl-gcc가
+ # onig_sys 링크에 *성공*할 수도 있으므로, 링크 실패가 순수 Rust를
+ # 지켜줄 거라 기대하지 말 것 — 플래그가 유일한 가드다.
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
+ flags: --no-default-features --features pure-rust
- os: macos-latest
target: aarch64-apple-darwin
+ flags: ""
runs-on: ${{ matrix.os }}
steps:
@@ -46,7 +51,7 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: Build
- run: cargo build --release --locked --target ${{ matrix.target }}
+ run: cargo build --release --locked --target ${{ matrix.target }} ${{ matrix.flags }}
# 빌드된 바이너리가 실제로 사이트를 만드는지 본다. 링크만 성공하고 런타임에
# 죽는 조합이 있으므로(정적 빌드에서 특히), 여기서 한 번 돌려 본다.
diff --git a/Cargo.lock b/Cargo.lock
@@ -874,6 +874,28 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+[[package]]
+name = "onig"
+version = "6.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2"
+dependencies = [
+ "bitflags",
+ "libc",
+ "once_cell",
+ "onig_sys",
+]
+
+[[package]]
+name = "onig_sys"
+version = "69.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -925,6 +947,12 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
[[package]]
name = "plist"
version = "1.10.0"
@@ -1282,6 +1310,7 @@ dependencies = [
"flate2",
"fnv",
"once_cell",
+ "onig",
"plist",
"regex-syntax",
"serde",
diff --git a/Cargo.toml b/Cargo.toml
@@ -27,7 +27,8 @@ blake3 = "1"
clap = { version = "4.6", features = ["derive"] }
# ⚠️ comrak의 default = ["cli", "syntect-onig", "bon"] 이다. 그대로 쓰면
# (1) comrak 자체 CLI 의존성(clap/shell-words/xdg/fmt2io)이 딸려오고
-# (2) syntect가 **Oniguruma(C 바인딩)**로 들어와 musl 정적 빌드/크로스컴파일이 깨진다.
+# (2) syntect가 comrak 경유로 무조건 Oniguruma에 고정된다 — 엔진 선택은
+# 아래 [features]의 몫이라(musl 아티팩트는 pure-rust여야 한다) 차단한다.
# SyntaxHighlighterAdapter / HeadingAdapter 트레이트는 cfg 게이트가 없어 피처 없이도
# 쓸 수 있고, 피처 뒤에 있는 건 comrak 내장 SyntectAdapter뿐인데 그건 우리가 직접
# 구현하므로(업스트림 #330이 재작업 대상으로 지적) 필요 없다.
@@ -37,20 +38,17 @@ notify = "8.2"
notify-debouncer-full = "0.7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
-syntect = { version = "5.3", default-features = false, features = ["default-fancy"] }
+# 구문 강조 엔진은 [features]에서 고른다. 여기서는 둘 다 피처를 비워 둔다 —
+# syntect의 엔진 피처를 직접 켜면 엔진=fancy + two-face 덤프=onig 같은 어긋남이
+# 가능하고, 그 조합은 컴파일은 되지만 일부 정규식에서 조용히 평문으로 강등된다.
+syntect = { version = "5.3", default-features = false }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal"] }
toml = "1.1"
tower-http = { version = "0.7", features = ["fs"] }
-two-face = { version = "0.5", default-features = false, features = ["syntect-fancy"] }
+two-face = { version = "0.5", default-features = false }
walkdir = "2"
# --- M1 (아래는 해당 기능 착수할 때 주석 해제) ---
-# syntect 기본 피처는 regex-onig(C 바인딩)이라 musl 정적 빌드/크로스컴파일이 깨진다.
-# default-fancy = 순수 Rust. 빌드 타임 작업이라 속도 차이는 무의미하다.
-# syntect = { version = "5.3", default-features = false, features = ["default-fancy"] }
-# two-face는 syntect 기본 세트에 없는 TOML/TypeScript/Dockerfile을 채운다(문서 사이트 필수).
-# 피처를 안 맞추면 onig가 다시 딸려오므로 syntect-fancy를 반드시 명시할 것.
-# two-face = { version = "0.5", default-features = false, features = ["syntect-fancy"] }
# blake3 = "1" # 에셋 콘텐츠 해시
# axum = "0.8" # 개발 서버
# tower-http = { version = "0.7", features = ["fs"] }
@@ -69,15 +67,22 @@ walkdir = "2"
# 2000페이지 한국어 코퍼스 실측: 정답 1018건 → 27건.
# pagefind = { version = "1.5", default-features = false, optional = true }
-# --- M3 이후 ---
-# rayon = "1" # 병렬 렌더. 문서 사이트 ~60페이지엔 불필요하고, 출력 URL 충돌 시
-# # 비결정성 문제를 같이 떠안게 된다. 페이지가 수백 개가 되면 그때.
-
[dev-dependencies]
insta = "1"
[features]
-default = []
+# 구문 강조의 정규식 엔진. 기본은 Oniguruma(C) — fancy 대비 코드 무거운 코퍼스
+# 실측 3.0배(1000p 전 블록 고유: 3.9s → 1.3s). musl 정적 아티팩트만 pure-rust로
+# 빌드한다(release.yml의 --no-default-features --features pure-rust — C 바인딩이
+# 정적 링크를 깨는 지점이 거기라서다). 둘 다 켜지면 syntect·two-face 모두 onig로
+# 수렴하므로(syntect regex.rs에 명시된 우선순위) --all-features(AUR)도 안전하다.
+# 두 덤프는 같지 않다: fancy 덤프는 문법 7개(PowerShell·JS(Babel)·Salt State·
+# ARM Assembly 등)를 통째로 빼서 해당 펜스가 평문 폴백이 되고, js 토큰조차
+# 두 빌드가 다른 문법(Babel vs plain JS)으로 해석한다 — installation.md에
+# 문서화된 계약이고, 같은 바이너리 안에서는 여전히 결정적이다.
+default = ["onig"]
+onig = ["syntect/default-onig", "two-face/syntect-onig"]
+pure-rust = ["syntect/default-fancy", "two-face/syntect-fancy"]
# search = ["dep:pagefind"]
[profile.release]
diff --git a/docs/content/start/installation.ko.md b/docs/content/start/installation.ko.md
@@ -23,8 +23,16 @@ cargo build --release
| 요구사항 | 버전 |
|---|---|
| Rust | 1.97 이상 |
+| C 컴파일러 | rustc가 링커 구동에 씁니다. 기본 빌드는 구문 강조의 정규식 엔진인 Oniguruma도 이걸로 컴파일합니다. |
| 그 외 | — |
+정적 릴리스가 싣는 구성 그대로 — 순수 Rust 정규식 엔진, C 소스 컴파일
+없음 — 는 플래그 하나 거리입니다:
+
+```bash
+cargo build --release --no-default-features --features pure-rust
+```
+
## 미리 빌드된 바이너리
[릴리스](https://github.com/sqzer-x/sqzass/releases)마다 정적 링크된
@@ -38,8 +46,15 @@ sudo install -m755 sqzass-*/sqzass /usr/local/bin/
```
리눅스 빌드는 정적이라 glibc를 요구하지 않고, 빌드한 배포판보다 오래된 배포판에서도
-돕니다. 이 프로젝트가 syntect를 순수 Rust 정규식 엔진으로 고정한 이유가 이것입니다.
-기본값은 C 바인딩이고, C 바인딩은 정확히 musl 정적 빌드를 깨뜨립니다.
+돕니다. 순수 Rust 정규식 엔진을 쓰는 유일한 빌드이기도 합니다 — 다른 모든 곳에서
+sqzass는 코드가 많은 사이트에서 눈에 띄게 빠른 Oniguruma로 강조하는데, Oniguruma는
+C 바인딩이고 C 바인딩은 정확히 musl 정적 빌드를 깨뜨립니다. 두 엔진의 문법 집합도
+같지 않습니다. 순수 Rust 엔진이 일부 문법의 정규식을 못 돌리기 때문에 정적
+아티팩트에는 문법 일곱 개가 통째로 빠집니다 — PowerShell, JavaScript (Babel),
+Salt State, ARM Assembly 등이요. 네이티브 빌드가 온전히 칠하는 ` ```powershell `
+이나 ` ```jsx ` 펜스가 정적 아티팩트에서는 에러 없이 평문이 됩니다. ` ```js `
+조차 마크업 구조가 다릅니다. 네이티브 빌드는 Babel 문법으로, 정적 빌드는 일반
+JavaScript로 해석합니다. 각 바이너리 자체는 여전히 완전히 결정적입니다.
> [!NOTE]
> 아직 릴리스가 없습니다 — 워크플로는 있고 첫 태그를 아직 붙이지 않았습니다.
diff --git a/docs/content/start/installation.md b/docs/content/start/installation.md
@@ -23,8 +23,16 @@ The binary lands at `target/release/sqzass`.
| Requirement | Version |
|---|---|
| Rust | 1.97 or newer |
+| A C compiler | rustc drives the linker with it; the default build also compiles Oniguruma, the highlighter's regex engine, with it. |
| Everything else | — |
+The exact configuration the static release ships — the pure-Rust regex
+engine, no C source compiled — is a feature flag away:
+
+```bash
+cargo build --release --no-default-features --features pure-rust
+```
+
## Prebuilt binaries
Attached to each [release](https://github.com/sqzer-x/sqzass/releases): a
@@ -38,9 +46,18 @@ sudo install -m755 sqzass-*/sqzass /usr/local/bin/
```
The Linux build is static, so it has no glibc requirement and runs on
-distributions older than the one it was built on. That is the reason this
-project pins syntect to its pure-Rust regex engine: the default one is a C
-binding, and a C binding is exactly what breaks a musl static build.
+distributions older than the one it was built on. It is also the one build
+that uses the pure-Rust regex engine: everywhere else sqzass highlights with
+Oniguruma, which is measurably faster on code-heavy sites, but Oniguruma is a
+C binding and a C binding is exactly what breaks a musl static build. The
+engines' grammar sets are not identical either: the pure-Rust engine cannot
+run a few grammars' regexes, so the static artifact drops seven of them
+entirely — PowerShell, JavaScript (Babel), Salt State and ARM Assembly among
+them. A ` ```powershell ` or ` ```jsx ` fence that a native build highlights
+in full falls back to plain text in the static artifact, without an error.
+Even ` ```js ` differs in markup structure: the native build resolves it to
+the Babel grammar, the static one to plain JavaScript. Each binary is still
+fully deterministic on its own.
> [!NOTE]
> There is no release yet — the workflow exists and the first tag has not been
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,5 +1,12 @@
//! sqzass — Rust로 만든 정적 사이트 생성기.
+// 엔진 피처 없이 빌드하면 two-face가 자기 피처 이름으로 에러를 내는데,
+// 사용자가 알아야 할 이름은 우리 것이다.
+#[cfg(not(any(feature = "onig", feature = "pure-rust")))]
+compile_error!(
+ "정규식 엔진 피처가 필요합니다: 기본(onig) 또는 --no-default-features --features pure-rust"
+);
+
pub mod assets;
pub mod config;
pub mod content;
|