blob: d2148b7fb41ffe2be81badebeb030258c03d06ef [file]
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Rewrite java-sdk/gradle/verification-metadata.xml from an empty component list.
Gradle only ever appends to that file, so a version bump leaves the superseded
entries behind and they stay trusted for good. Starting from an empty list drops
them, which is what turns a stale checksum into a visible diff.
"""
from __future__ import annotations
import pathlib
import subprocess
import sys
import time
from collections.abc import Callable
REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
JAVA_SDK = REPO_ROOT / "java-sdk"
METADATA = JAVA_SDK / "gradle" / "verification-metadata.xml"
GRADLE_TASKS = [
"build",
":sdk:dokkaGeneratePublicationHtml",
":sdk:dokkaGeneratePublicationJavadoc",
"sourceTarball",
"checksumSourceTarball",
"publishToMavenLocal",
]
MAX_ATTEMPTS = 3
RETRY_DELAY_SECONDS = 30
LICENSE_HEADER = """<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->"""
ADVISORY = """
The trust list changed. Entries that disappeared are checksums nothing resolves
any more - usually versions superseded by a dependency bump. They stayed
trusted, so a future direct or transitive dependency could have pulled that
exact version back in with nobody reviewing its checksum.
Review every entry in the diff before staging it: generating the file records
what the repositories served, it does not make those bytes trustworthy.
"""
class RegenerationFailedError(RuntimeError):
"""Gradle could not regenerate the metadata within the attempt budget."""
def build_empty_metadata(committed: str) -> str:
"""Return the committed metadata with its component list emptied."""
kept: list[str] = []
for line in committed.splitlines():
if "<components>" in line:
kept += [" <components/>", "</verification-metadata>"]
break
kept.append(line)
return "\n".join(kept) + "\n"
def insert_license_header(metadata: str) -> str:
"""Put the ASF header back after the XML declaration, unless Gradle kept one."""
if "Licensed to the Apache Software Foundation" in metadata:
return metadata
declaration, *rest = metadata.splitlines()
return "\n".join([declaration, LICENSE_HEADER, *rest]) + "\n"
def run_gradle() -> bool:
"""Both properties are needed for the run to reach the end: signing has no key
here, and sourceTarball has no default ref."""
result = subprocess.run(
[
"./gradlew",
"--no-daemon",
"--write-verification-metadata",
"sha256",
"--refresh-dependencies",
*GRADLE_TASKS,
"-PskipSigning=true",
"-PgitRef=HEAD",
],
cwd=JAVA_SDK,
check=False,
)
return result.returncode == 0
def regenerate(
metadata: pathlib.Path,
gradle: Callable[[], bool],
sleep: Callable[[float], None] = time.sleep,
) -> None:
"""Rewrite the metadata in place, putting the committed file back if the run never succeeds."""
committed = metadata.read_text()
restore = True
try:
for attempt in range(1, MAX_ATTEMPTS + 1):
print(f"==> Regenerating verification metadata (attempt {attempt}/{MAX_ATTEMPTS})", flush=True)
metadata.write_text(build_empty_metadata(committed))
if gradle():
metadata.write_text(insert_license_header(metadata.read_text()))
restore = False
return
if attempt < MAX_ATTEMPTS:
print(f"Regeneration failed, retrying in {RETRY_DELAY_SECONDS}s", file=sys.stderr, flush=True)
sleep(RETRY_DELAY_SECONDS)
raise RegenerationFailedError(f"Regeneration failed after {MAX_ATTEMPTS} attempts")
finally:
if restore:
metadata.write_text(committed)
def metadata_changed(metadata: pathlib.Path) -> bool:
result = subprocess.run(["git", "diff", "--quiet", "--", str(metadata)], cwd=REPO_ROOT, check=False)
return result.returncode != 0
def main() -> int:
try:
regenerate(METADATA, run_gradle)
except RegenerationFailedError as error:
print(f"ERROR: {error}", file=sys.stderr)
return 1
if metadata_changed(METADATA):
print(ADVISORY, file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())