diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 559d49d64a5d..d4c4fa66656a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,11 +50,10 @@ jobs: - {VERSION: "3.14", NOXSESSION: "tests", OPENSSL: {TYPE: "openssl", VERSION: "f31510e95333b33a8765cbb81a147df7572c88b2"}} # Builds with various Rust versions. Includes MSRV and next # potential future MSRV. - # - 1.85: 2024 edition # - 1.95: cfg_select - - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "1.83.0"} - - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "1.83.0", OPENSSL: {TYPE: "aws-lc", VERSION: "v5.1.0"}} - - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "1.83.0", OPENSSL: {TYPE: "boringssl", VERSION: "1c7d52ef3e3f373302cb957089fa783d1e5fd8cd"}} + - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "1.85.0"} + - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "1.85.0", OPENSSL: {TYPE: "aws-lc", VERSION: "v5.1.0"}} + - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "1.85.0", OPENSSL: {TYPE: "boringssl", VERSION: "1c7d52ef3e3f373302cb957089fa783d1e5fd8cd"}} - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "beta"} - {VERSION: "3.14", NOXSESSION: "rust,tests", RUST: "nightly"} - {VERSION: "3.14", NOXSESSION: "tests-rust-debug"} diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fa3f1e0c538d..361fba2bc113 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -39,6 +39,7 @@ Changelog classes and the classes in :mod:`~cryptography.hazmat.primitives.asymmetric.padding` can now be compared with ``==``. +* Updated the minimum supported Rust version (MSRV) to 1.85.0, from 1.83.0. .. _v49-0-0: diff --git a/Cargo.toml b/Cargo.toml index 540a19e4f089..b41a08dcfe85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ authors = ["The cryptography developers "] edition = "2021" publish = false # This specifies the MSRV -rust-version = "1.83.0" +rust-version = "1.85.0" license = "Apache-2.0 OR BSD-3-Clause" [workspace.dependencies] diff --git a/docs/installation.rst b/docs/installation.rst index 34b86cdd6c1c..3b5b51e8433b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -110,7 +110,7 @@ available`. .. warning:: - The Rust available by default in Alpine < 3.21 is older than the + The Rust available by default in Alpine < 3.22 is older than the minimum supported version. See the :ref:`Rust installation instructions ` for information about installing a newer Rust. @@ -137,9 +137,9 @@ available`. .. warning:: - For RHEL and CentOS you must be on version 9.6 or newer for the command + For RHEL and CentOS you must be on version 9.7 or newer for the command below to install a sufficiently new Rust. If your Rust is less than - 1.83.0 please see the :ref:`Rust installation instructions + 1.85.0 please see the :ref:`Rust installation instructions ` for information about installing a newer Rust. .. code-block:: console @@ -321,7 +321,7 @@ Rust a Rust toolchain. Building ``cryptography`` requires having a working Rust toolchain. The current -minimum supported Rust version is 1.83.0. **This is newer than the Rust some +minimum supported Rust version is 1.85.0. **This is newer than the Rust some package managers ship**, so users may need to install with the instructions below. diff --git a/noxfile.py b/noxfile.py index efeed320a3fb..0a63cf39f57c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -347,6 +347,111 @@ def local(session: nox.Session): BIN_EXT = ".exe" if sys.platform == "win32" else "" +def _parse_profdata_function_counts(prof_dump: str) -> dict[str, int]: + # Parses `llvm-profdata show --all-functions` output, which contains + # a block like this for every instrumented function: + # : + # Hash: 0x0123456789abcdef + # Counters: 7 + # Function count: 8 + counts: dict[str, int] = {} + name = None + for line in prof_dump.splitlines(): + if line.startswith(" ") and not line.startswith(" "): + name = line.strip().removesuffix(":") + elif name is not None and line.strip().startswith("Function count: "): + count = int(line.rsplit(":", 1)[1]) + counts[name] = max(counts.get(name, 0), count) + name = None + return counts + + +def _repair_expanded_code_coverage( + lcov_data: str, prof_counts: dict[str, int] +) -> str: + """ + Starting with Rust 1.84, a function consisting entirely of + proc-macro expanded code (e.g. the impls generated by + ``#[derive(...)]`` or ``#[pyo3::pymethods]``) gets a coverage + mapping containing only a residual region on the attribute line, + wired to a counter that never executes. llvm-cov therefore reports + the function -- and with it the attribute line -- as unexecuted, + even when the raw profile proves the function ran (its counters + are live; only the mapping is defective). We repair the two ways + this corrupts line coverage: + + - If the raw profile's count for a function disagrees with the + lcov function summary (FNDA), trust the profile and mark the + function's start line as executed. + - If every function on a zero-count line is absent from the raw + profile, the compiler emitted only "unused function" placeholder + records for macro-generated impls that were never codegen'd + (e.g. an unused generated trait impl); the line's executability + is an artifact of the residual region, so drop it. Handwritten + dead code is unaffected: it is codegen'd, so it registers a + profile record (with count 0) and its body lines keep their + zero-count records either way. + """ + result: list[str] = [] + record: list[str] = [] + for line in lcov_data.splitlines(): + record.append(line) + if line == "end_of_record": + result.extend(_repair_lcov_record(record, prof_counts)) + record = [] + assert not record + return "".join(f"{line}\n" for line in result) + + +def _repair_lcov_record( + record: list[str], prof_counts: dict[str, int] +) -> list[str]: + fn_start_lines: dict[str, int] = {} + mapped_counts: dict[str, int] = {} + for line in record: + if line.startswith("FN:"): + start, _, name = line.removeprefix("FN:").partition(",") + fn_start_lines[name] = int(start) + elif line.startswith("FNDA:"): + count, _, name = line.removeprefix("FNDA:").partition(",") + mapped_counts[name] = max(mapped_counts.get(name, 0), int(count)) + + repaired_lines: dict[int, int] = {} + line_has_profiled_fn: dict[int, bool] = {} + for fn_name, fn_start in fn_start_lines.items(): + if fn_name in prof_counts: + line_has_profiled_fn[fn_start] = True + else: + line_has_profiled_fn.setdefault(fn_start, False) + if mapped_counts.get(fn_name) == 0: + true_count = prof_counts.get(fn_name, 0) + if true_count > 0: + repaired_lines[fn_start] = max( + repaired_lines.get(fn_start, 0), true_count + ) + unmapped_lines = { + start + for start, profiled in line_has_profiled_fn.items() + if not profiled + } + if not repaired_lines and not unmapped_lines: + return record + + result = [] + for line in record: + if line.startswith("DA:"): + line_number, _, count = line.removeprefix("DA:").partition(",") + if int(count) == 0: + if int(line_number) in repaired_lines: + line = ( + f"DA:{line_number},{repaired_lines[int(line_number)]}" + ) + elif int(line_number) in unmapped_lines: + continue + result.append(line) + return result + + def process_rust_coverage( session: nox.Session, rust_binaries: list[str], @@ -393,5 +498,17 @@ def process_rust_coverage( lambda m: "SF:src/rust/" + m.group(1).replace("\\", "/"), lcov_data.replace("\r\n", "\n"), ) + prof_dump = session.run( + str(target_bindir / ("llvm-profdata" + BIN_EXT)), + "show", + "--all-functions", + "rust-cov.profdata", + silent=True, + external=True, + ) + assert isinstance(prof_dump, str) + lcov_data = _repair_expanded_code_coverage( + lcov_data, _parse_profdata_function_counts(prof_dump) + ) with open(f"{uuid.uuid4()}.lcov", "w") as f: f.write(lcov_data)