blob: 0a33472ded1eb7f5f2a9f0966b06f5a00c3fc6ee [file] [edit]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2021 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Updates the Fuchsia platform version.
"""
import argparse
import fileinput
import glob
import json
import os
import pathlib
import secrets
import shutil
import sys
def _generate_random_abi_revision(already_used: set[int]) -> int:
"""Generates a random 64-bit ABI revision, avoiding values in
`already_used` and other reserved ranges."""
while True:
# Reserve values in the top 1/256th of the space.
candidate = secrets.randbelow(0xF000_0000_0000_0000)
# If the ABI revision has already been used, discard it and go buy a
# lottery ticket.
if candidate not in already_used:
return candidate
def update_version_history(
new_api_level: int, version_history_path: str
) -> bool:
"""Updates version_history.json to include the given Fuchsia API level.
The ABI revision for this API level is set to a new random value that has not
been used before.
"""
try:
with open(version_history_path, "r+") as f:
version_history = json.load(f)
versions = version_history["data"]["api_levels"]
if str(new_api_level) in versions:
print(
f"error: Fuchsia API level {new_api_level} is already defined.",
file=sys.stderr,
)
return False
abi_revision = _generate_random_abi_revision(
set(int(v["abi_revision"], 16) for v in versions.values())
)
versions[str(new_api_level)] = dict(
# Print `abi_revision` in hex, with a leading 0x, with capital
# letters, padded to 16 chars.
abi_revision=f"0x{abi_revision:016X}",
phase="supported",
)
f.seek(0)
json.dump(version_history, f, indent=4)
# JSON dump does not include a new line at the end.
f.write("\n")
f.truncate()
return True
except FileNotFoundError:
print(
f"""error: Unable to open '{version_history_path}'.
Did you run this script from the root of the source tree?""",
file=sys.stderr,
)
return False
def _create_owners_file(level_dir_path: str) -> None:
"""Creates an OWNERS file in `level_dir_path` with limited approvers."""
owners_path = os.path.join(level_dir_path, "OWNERS")
print(f"Creating {owners_path}")
with open(owners_path, "w") as f:
f.write("include /sdk/history/FROZEN_API_LEVEL_OWNERS\n")
def _update_availability_levels(
new_api_level: int, availability_levels_file: str
) -> bool:
"""Adds the given Fuchsia API level to the C preprocessor defines file.
Assumes lines are sorted in decreasing order of API level.
"""
# Look for the most recent line and replace it with the line for the new
# level and itself.
last_api_level = new_api_level - 1
line_format = "#define FUCHSIA_INTERNAL_LEVEL_{level}_() {level}"
most_recent_api_level_line = line_format.format(level=last_api_level)
new_line = line_format.format(level=new_api_level)
try:
with fileinput.FileInput(availability_levels_file, inplace=True) as f:
for line in f:
print(
line.replace(
most_recent_api_level_line,
f"{new_line}\n{most_recent_api_level_line}",
),
end="",
)
return True
except FileNotFoundError:
print(
f"""error: Unable to open '{availability_levels_file}'.
Did you run this script from the root of the source tree?""",
file=sys.stderr,
)
return False
def create_sdk_history(root_source_dir: str, new_level: int) -> None:
level_dir_path = os.path.join(
root_source_dir, "sdk", "history", str(new_level)
)
try:
os.makedirs(level_dir_path, exist_ok=True)
except Exception as e:
print(f"Failed to create directory for new level: {e}")
sys.exit(1)
_create_owners_file(level_dir_path)
next_dir_path = os.path.join(root_source_dir, "sdk", "history", "NEXT")
# The BUILD.bazel file just exposes the golden files in the directory to
# Bazel targets, so the same file can be used for every level.
shutil.copy(
os.path.join(next_dir_path, "BUILD.bazel"),
os.path.join(level_dir_path, "BUILD.bazel"),
)
ifs_files = glob.glob(
"*.ifs",
root_dir=next_dir_path,
)
# The .ifs files cannot be generated from scratch because they are also used as source files.
# This creates empty files based on the contents of NEXT allowing the build to succeed and write the files with the correct contents.
for ifs_file in ifs_files:
# make an empty file
with open(os.path.join(level_dir_path, ifs_file), "w"):
pass
SDK_VERSION_HISTORY_PATH = "sdk/version_history.json"
AVAILABILITY_LEVELS_INC_PATH = (
"zircon/system/public/zircon/availability_levels.inc"
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--new-api-level", type=int, required=True)
parser.add_argument("--root-source-dir", type=pathlib.Path, required=True)
args = parser.parse_args()
new_level = args.new_api_level
if not update_version_history(
new_level, args.root_source_dir.joinpath(SDK_VERSION_HISTORY_PATH)
):
return 1
create_sdk_history(args.root_source_dir, new_level)
# TODO(https://fxbug.dev/349622444): Enable building with
# `FUCHSIA_INTERNAL_LEVEL_NEXT_()` undefined to ensure there are no stray
# instances of `NEXT` that have not been converted to the new level.
# Otherwise, perhaps use a preprocessor condition. This would avoid the need
# to regenerate `availability_levels_file_path` twice but would include a
# preprocessor condition in the production code. Do the same for the Rust
# macros.
if not _update_availability_levels(
new_level, args.root_source_dir.joinpath(AVAILABILITY_LEVELS_INC_PATH)
):
return 1
print(
f"""
API level {new_level} has been added.
"""
)
return 0
if __name__ == "__main__":
sys.exit(main())