diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e8a05c..7ae274e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,16 @@ on: env: CARGO_TERM_COLOR: always RUSTFLAGS: -D warnings + # Every blocking job pins an exact toolchain. With `-D warnings` and a + # floating `stable`, a Rust release turns this gate red with no repo change — + # which is exactly how it first failed, eight days after 1.97.1 added + # `float_literal_f32_fallback`. Worse, the developer machine cannot install + # arbitrary toolchains, so such a failure is unreproducible locally and the + # local gate stops meaning anything. Pinned here to the version developed + # against; bump it deliberately, as its own commit, with the lints it + # introduces fixed in that commit. The nightly soak floats on `stable` and is + # the drift detector. + PINNED_STABLE: "1.95.0" jobs: check: @@ -27,7 +37,7 @@ jobs: - name: Install Rust (pinned MSRV) uses: dtolnay/rust-toolchain@stable with: - toolchain: "1.77" + toolchain: "1.85" components: rustfmt, clippy - name: Cache cargo @@ -56,18 +66,20 @@ jobs: RUSTDOCFLAGS: -D warnings run: cargo doc --workspace --exclude epiphany-editor-gui --no-deps - # The demo GUI crate, on current stable (its eframe/resvg tree requires a newer - # toolchain than the workspace MSRV). Separate so a GUI build break is its own - # attributable signal, not a regression in the core crates. + # The demo GUI crate, on the pinned stable rather than the MSRV: its + # eframe/resvg tree pulls `image`, which declares 1.88 — three releases above + # the workspace floor. Separate so a GUI build break is its own attributable + # signal, not a regression in the core crates. editor-gui: - name: editor GUI (current stable) + name: editor GUI (pinned stable) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Install Rust (current stable) + - name: Install Rust (pinned stable) uses: dtolnay/rust-toolchain@stable with: + toolchain: ${{ env.PINNED_STABLE }} components: clippy - name: Install GUI build dependencies @@ -94,6 +106,8 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ env.PINNED_STABLE }} - name: Cache cargo uses: Swatinem/rust-cache@v2 @@ -131,6 +145,8 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ env.PINNED_STABLE }} - name: Cache cargo uses: Swatinem/rust-cache@v2 @@ -139,6 +155,10 @@ jobs: run: cargo test -p epiphany-testkit --test prepass # A longer soak, scheduled nightly, exploring a wider seed space at scale. + # This job deliberately floats on `stable` while every blocking job is pinned: + # it is where toolchain drift surfaces, on a schedule, instead of in a push + # that has nothing to do with it. A red soak against a green push means Rust + # moved, not that the change did. soak: name: conformance soak (nightly) if: github.event_name == 'schedule' diff --git a/Cargo.toml b/Cargo.toml index fb854e3..65a732b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,12 @@ members = [ [workspace.package] edition = "2021" -rust-version = "1.77" +# 1.85 is the floor the dependency graph actually imposes, not a preference: +# blake3 and its `constant_time_eq` both ship edition 2024, which no Cargo +# before 1.85 can parse. The previous 1.77 claim was untestable until CI +# existed and was false when it ran — raise this only against a measured +# closure (`cargo metadata --filter-platform`), never to make a build pass. +rust-version = "1.85" authors = ["Epiphany contributors"] repository = "https://github.com/epiphany-notation/epiphany" diff --git a/crates/epiphany-core/src/invariants.rs b/crates/epiphany-core/src/invariants.rs index de15cbb..1e29086 100644 --- a/crates/epiphany-core/src/invariants.rs +++ b/crates/epiphany-core/src/invariants.rs @@ -1332,7 +1332,7 @@ impl<'a> GraphIndex<'a> { TempoShape::Constant => flag( seg.end_tempo .as_ref() - .map_or(true, |et| et == &seg.start_tempo), + .is_none_or(|et| et == &seg.start_tempo), "constant tempo segment has end_tempo != start_tempo".to_string(), ), TempoShape::Linear | TempoShape::Exponential | TempoShape::Curve => flag( diff --git a/crates/epiphany-editor-gui/src/main.rs b/crates/epiphany-editor-gui/src/main.rs index e591157..06c4345 100644 --- a/crates/epiphany-editor-gui/src/main.rs +++ b/crates/epiphany-editor-gui/src/main.rs @@ -484,7 +484,9 @@ impl EditorApp { ui.painter().rect_stroke( shape_rect(®ion.shape, &vm), 0.0, - egui::Stroke::new(2.0, egui::Color32::from_rgb(0, 120, 215)), + // Suffixed: `float_literal_f32_fallback` (Rust 1.97) rejects + // inferring `f32` here, and it becomes a hard error later. + egui::Stroke::new(2.0_f32, egui::Color32::from_rgb(0, 120, 215)), ); } } diff --git a/crates/epiphany-engrave/src/casting.rs b/crates/epiphany-engrave/src/casting.rs index e2b048d..46b3055 100644 --- a/crates/epiphany-engrave/src/casting.rs +++ b/crates/epiphany-engrave/src/casting.rs @@ -1516,7 +1516,7 @@ fn stroke_fate( } else { 0.0 }; - if best.map_or(true, |(_, d)| distance < d) { + if best.is_none_or(|(_, d)| distance < d) { best = Some((r, distance)); } } @@ -1595,7 +1595,7 @@ fn curve_fate( for (r, span) in region_spans.iter().enumerate() { let Some((rlo, rhi)) = span else { continue }; let distance = interval_distance(lo, hi, (*rlo, *rhi)); - if best.map_or(true, |(_, d)| distance < d) { + if best.is_none_or(|(_, d)| distance < d) { best = Some((r, distance)); } } diff --git a/crates/epiphany-ops/src/reduce.rs b/crates/epiphany-ops/src/reduce.rs index 999cd5d..7627ab9 100644 --- a/crates/epiphany-ops/src/reduce.rs +++ b/crates/epiphany-ops/src/reduce.rs @@ -824,7 +824,7 @@ fn tempo_segment_shape_well_formed(segment: &TempoSegment) -> bool { TempoShape::Constant => segment .end_tempo .as_ref() - .map_or(true, |end| end == &segment.start_tempo), + .is_none_or(|end| end == &segment.start_tempo), TempoShape::Linear | TempoShape::Exponential | TempoShape::Curve => { segment.end_tempo.is_some() && segment.end.is_some() } @@ -6620,7 +6620,7 @@ impl<'a> Reducer<'a> { (0u8, signed) }; let key = (rank, distance, direction, *event); - if best.as_ref().map_or(true, |current| key < *current) { + if best.as_ref().is_none_or(|current| key < *current) { best = Some(key); } } @@ -7425,7 +7425,7 @@ fn first_missing_vector_predecessor( if !complete { let candidate = OperationId::new(replica, expected); - if first_missing.map_or(true, |current| candidate < current) { + if first_missing.is_none_or(|current| candidate < current) { first_missing = Some(candidate); } }