Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
groups:
cargo-minor:
update-types: [minor, patch]

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
github-actions:
patterns: ['*']
137 changes: 137 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
CARGO_TERM_COLOR: always

jobs:
fmt:
name: rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- run: cargo fmt --all --check

clippy:
name: clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- run: cargo clippy --locked --all-targets --all-features -- -D warnings

# macOS/Windows はユニットテストとビルド健全性のみ (--bins)。tests/ 配下の統合・E2E テストは
# GCC 専用 (<bits/stdc++.h> や実 GCC ライブラリ) で clang/MinGW では動かないため、ここでは走らせない。
# それらフルスイートは下の coverage ジョブ (Linux + g++ + submodules) で実行している。
test:
name: test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, macos-26-intel, windows-latest]
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- run: cargo test --locked --all-features --bins
Comment thread
TwoSquirrels marked this conversation as resolved.

# Linux のフルスイート (ユニット + tests/ の統合・E2E) を計測し、Pages へカバレッジを公開する。
coverage:
name: coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
submodules: recursive
persist-credentials: false
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- run: rustup component add llvm-tools-preview
- uses: taiki-e/install-action@0631aa6515c7d545823c67cfae7ef4fc7f490154 # v2.81.8
with:
tool: cargo-llvm-cov

- name: Collect coverage
run: cargo llvm-cov --locked --all-features --no-report

- name: Build report and badge
run: |
cargo llvm-cov report --html --output-dir target/llvm-cov
pct=$(cargo llvm-cov report --json --summary-only \
| jq '.data[0].totals.lines.percent')
rounded=$(awk -v p="$pct" 'BEGIN { printf "%.1f", p }')
color=$(awk -v p="$pct" 'BEGIN {
if (p >= 90) print "brightgreen"
else if (p >= 75) print "green"
else if (p >= 60) print "yellowgreen"
else if (p >= 40) print "yellow"
else if (p >= 20) print "orange"
else print "red"
}')
mkdir -p _site
cp -r target/llvm-cov/html/. _site/
jq -n --arg m "${rounded}%" --arg c "$color" \
'{schemaVersion: 1, label: "coverage", message: $m, color: $c}' > _site/badge.json

- name: Upload Pages artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: _site

deploy-pages:
name: deploy coverage to Pages
needs: coverage
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
concurrency:
group: pages
cancel-in-progress: false
steps:
- id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0

package:
name: package (publish dry-run)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- run: cargo publish --locked --dry-run

audit:
name: cargo audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: taiki-e/install-action@0631aa6515c7d545823c67cfae7ef4fc7f490154 # v2.81.8
with:
tool: cargo-audit
- run: cargo audit
125 changes: 125 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: Release

on:
push:
tags:
- "v*"

permissions:
contents: read

env:
CARGO_TERM_COLOR: always

jobs:
validate:
name: validate tag
runs-on: ubuntu-latest
outputs:
release: ${{ steps.check.outputs.release }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Validate SemVer tag and Cargo.toml version
id: check
run: |
semver='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'
if [[ ! "$GITHUB_REF_NAME" =~ $semver ]]; then
echo "::notice::tag '$GITHUB_REF_NAME' is not a SemVer release tag; skipping release"
echo "release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
tag="${GITHUB_REF_NAME#v}"
crate="$(cargo metadata --no-deps --format-version 1 \
| jq -r '.packages[0].version')"
if [ "$tag" != "$crate" ]; then
echo "::error::tag v$tag does not match Cargo.toml version $crate"
exit 1
fi
echo "release=true" >> "$GITHUB_OUTPUT"

# タグ push は CI (push/PR トリガー) を起動しないため、公開前にここでフルスイートを回して
# リリースをテストでゲートする。E2E のため submodules + g++ (ubuntu-latest に同梱) を使う。
# リリース文脈では cache を使わない (cache-poisoning を避け、素の環境で検証する)。
test:
name: test
needs: validate
if: needs.validate.outputs.release == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
submodules: recursive
persist-credentials: false
- run: cargo test --locked --all-features

publish:
name: publish to crates.io
needs: [validate, test]
if: needs.validate.outputs.release == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Authenticate with crates.io
id: auth
uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4

- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
run: cargo publish --locked
Comment thread
TwoSquirrels marked this conversation as resolved.

binaries:
name: binary (${{ matrix.target }})
needs: [validate, publish]
if: needs.validate.outputs.release == 'true'
runs-on: ${{ matrix.os }}
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: macos-26-intel
target: x86_64-apple-darwin
- os: macos-latest
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Build
run: cargo build --locked --release --target ${{ matrix.target }}

- name: Archive (Unix)
if: runner.os != 'Windows'
run: |
name="risundle-${GITHUB_REF_NAME}-${{ matrix.target }}"
tar czf "${name}.tar.gz" -C "target/${{ matrix.target }}/release" risundle
echo "ASSET=${name}.tar.gz" >> "$GITHUB_ENV"

- name: Archive (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$name = "risundle-$env:GITHUB_REF_NAME-${{ matrix.target }}"
Compress-Archive -Path "target/${{ matrix.target }}/release/risundle.exe" -DestinationPath "$name.zip"
"ASSET=$name.zip" | Out-File -FilePath $env:GITHUB_ENV -Append

- name: Upload to release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
files: ${{ env.ASSET }}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

**Tree-Shaking 機能付き、競技プログラミング用 C++ ソースバンドラー**

[![CI](https://github.com/TwoSquirrels/risundle/actions/workflows/ci.yml/badge.svg)](https://github.com/TwoSquirrels/risundle/actions/workflows/ci.yml)
[![coverage](https://img.shields.io/endpoint?url=https://twosquirrels.github.io/risundle/badge.json)](https://twosquirrels.github.io/risundle/)
[![crates.io](https://img.shields.io/crates/v/risundle.svg)](https://crates.io/crates/risundle)
[![license](https://img.shields.io/crates/l/risundle.svg)](LICENSE)

Expand Down
5 changes: 2 additions & 3 deletions src/bundle/inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,8 @@ impl Inventory {
let TagsKind::Library { hash: expected, .. } = &lib.kind else {
continue; // std は検証対象外
};
let actual = hash::aggregate(&lib.path).with_context(|| {
format!("failed to recompute the hash of library `{}`", lib.id)
})?;
let actual = hash::aggregate(&lib.path)
.with_context(|| format!("failed to recompute the hash of library `{}`", lib.id))?;
if &actual != expected {
bail!(
"library `{0}` has changed since registration; run `risundle library update {0}` to update it",
Expand Down
5 changes: 4 additions & 1 deletion src/bundle/rewrite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ pub fn rewrite(
// 示すため保留中の linemarker をここで確定出力する。ダミー自身を指す marker は上で捨て済み
// なので、ここで flush される pending は include の出所を指す実 marker (例: `#line 5 "main.cpp"`)。
flush_marker(&mut output, &mut pending, &mut presumed, &display);
push_line(&mut output, restore_include(line).as_deref().unwrap_or(line));
push_line(
&mut output,
restore_include(line).as_deref().unwrap_or(line),
);
presumed.advance();
}
}
Expand Down
17 changes: 13 additions & 4 deletions src/commands/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,9 @@ fn resolve_source_root(path: &Path) -> Result<PathBuf> {
/// 含む ID を許すと意図しない場所を読み書きしてしまう。フェイルファストで早期に弾く。
fn validate_id(id: &str) -> Result<()> {
if id.is_empty() || id == "." || id == ".." || id.contains('/') || id.contains('\\') {
bail!("library ID `{id}` is not allowed (empty, `.`/`..`, or IDs containing path separators are rejected)");
bail!(
"library ID `{id}` is not allowed (empty, `.`/`..`, or IDs containing path separators are rejected)"
);
}
Ok(())
}
Expand Down Expand Up @@ -295,7 +297,9 @@ fn update_one(store: &LocalStore, id: &str, path: Option<&Path>) -> Result<()> {
match tags.kind {
TagsKind::Std { compilers } => {
if path.is_some() {
bail!("a path cannot be specified for the standard library (it is auto-detected from the compiler)");
bail!(
"a path cannot be specified for the standard library (it is auto-detected from the compiler)"
);
}
let discovered = discover_all(&compilers)?;
register_std(store, &discovered)?;
Expand Down Expand Up @@ -355,7 +359,10 @@ fn show(store: &LocalStore, id: &str, verbose: bool) -> Result<()> {
}
TagsKind::Library { hash, files } => {
show_field("Kind", "library");
show_field("Files", &format!("{} with defined identifiers", files.len()));
show_field(
"Files",
&format!("{} with defined identifiers", files.len()),
);
if verbose {
show_field("Hash", hash);
println!("Definitions:");
Expand Down Expand Up @@ -478,8 +485,10 @@ mod tests {
End of search list.\n\
trailing junk\n";
// 実在する dir のみ realpath 化される。"." はカレントなので拾われる。
// 期待値も同じく canonicalize する: Windows の verbatim パス (`\\?\`) や macOS の
// symlink (/tmp→/private/tmp) で表記が分岐するため、関数と同じ正規化を通して比較する。
let dirs = parse_search_dirs(verbose);
assert_eq!(dirs, vec![std::env::current_dir().unwrap()]);
assert_eq!(dirs, vec![Path::new(".").canonicalize().unwrap()]);
}

#[test]
Expand Down
12 changes: 8 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,12 @@ pub fn resolve(start_file: &Path) -> Result<Config> {

fn find_config_file(start_file: &Path) -> Result<Option<PathBuf>> {
// ancestors() が機能するよう絶対パス化する。相対パスのままだと親を辿れない。
let absolute = std::path::absolute(start_file)
.with_context(|| format!("failed to make an absolute path for {}", start_file.display()))?;
let absolute = std::path::absolute(start_file).with_context(|| {
format!(
"failed to make an absolute path for {}",
start_file.display()
)
})?;
// 先頭 (ファイル自身) を除いた親ディレクトリ群を、近い順に探索する。
let found = absolute
.ancestors()
Expand All @@ -60,8 +64,8 @@ fn find_config_file(start_file: &Path) -> Result<Option<PathBuf>> {
fn load(path: &Path) -> Result<Config> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {}", path.display()))?;
let raw: RawConfig = toml::from_str(&text)
.with_context(|| format!("failed to parse {}", path.display()))?;
let raw: RawConfig =
toml::from_str(&text).with_context(|| format!("failed to parse {}", path.display()))?;
Ok(raw.into())
}

Expand Down
6 changes: 3 additions & 3 deletions src/fs/relpath.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ pub fn to_slash(relative: &Path) -> Result<String> {
let Component::Normal(name) = component else {
continue;
};
let name = name.to_str().with_context(|| {
format!("file name is not valid UTF-8: {}", relative.display())
})?;
let name = name
.to_str()
.with_context(|| format!("file name is not valid UTF-8: {}", relative.display()))?;
parts.push(name);
}
Ok(parts.join("/"))
Expand Down
Loading
Loading