#!/usr/bin/env python3 # # Copyright (C) 2020 The Android Open Source Project # # Licensed 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. """Add files to a Rust package for third party review."""
import collections import datetime import enum import glob import json import os import pathlib import re
# patterns to match keys in Cargo.toml
NAME_PATTERN = r"^name *= *\"(.+)\""
NAME_MATCHER = re.compile(NAME_PATTERN)
VERSION_PATTERN = r"^version *= *\"(.+)\""
VERSION_MATCHER = re.compile(VERSION_PATTERN)
DESCRIPTION_PATTERN = r"^description *= *(\".+\")"
DESCRIPTION_MATCHER = re.compile(DESCRIPTION_PATTERN) # NOTE: This description one-liner pattern fails to match # multi-line descriptions in some Rust crates, e.g. shlex.
LICENSE_PATTERN = r"^license *= *\"(.+)\""
LICENSE_MATCHER = re.compile(LICENSE_PATTERN)
# patterns to match year/month/day in METADATA
YMD_PATTERN = r"^ +(year|month|day): (.+)$"
YMD_MATCHER = re.compile(YMD_PATTERN)
YMD_LINE_PATTERN = r"^.* year: *([^ ]+) +month: *([^ ]+) +day: *([^ ]+).*$"
YMD_LINE_MATCHER = re.compile(YMD_LINE_PATTERN)
# patterns to match different licence types in LICENSE*
APACHE_PATTERN = r"^.*Apache License.*$"
APACHE_MATCHER = re.compile(APACHE_PATTERN)
BOOST_PATTERN = r"^.Boost Software License.*Version 1.0.*$"
BOOST_MATCHER = re.compile(BOOST_PATTERN)
MIT_PATTERN = r"^.*MIT License.*$"
MIT_MATCHER = re.compile(MIT_PATTERN)
BSD_PATTERN = r"^.*BSD .*License.*$"
BSD_MATCHER = re.compile(BSD_PATTERN)
MPL_PATTERN = r"^.Mozilla Public License.*$"
MPL_MATCHER = re.compile(MPL_PATTERN)
UNLICENSE_PATTERN = r"^.*unlicense\.org.*$"
UNLICENSE_MATCHER = re.compile(UNLICENSE_PATTERN)
ZERO_BSD_PATTERN = r"^.*Zero-Clause BSD.*$"
ZERO_BSD_MATCHER = re.compile(ZERO_BSD_PATTERN)
ZLIB_PATTERN = r"^.*zlib License.$"
ZLIB_MATCHER = re.compile(ZLIB_PATTERN)
MULTI_LICENSE_COMMENT = ("# Dual-licensed, using the least restrictive " "per go/thirdpartylicenses#same.\n ")
# default owners added to OWNERS
DEFAULT_OWNERS = "include platform/prebuilts/rust:main:/OWNERS\n"
# See b/159487435 Official policy for rust imports METADATA URLs. # "license_type: NOTICE" might be optional, # but it is already used in most rust crate METADATA. # This line format should match the output of external_updater.
METADATA_CONTENT = """name: "{name}"
description: {description}
third_party {{
identifier {{
type: "crates.io"
value: "{name}"
}}
identifier {{
type: "Archive"
value: "https://static.crates.io/crates/{name}/{name}-{version}.crate"
primary_source: true
}}
version: "{version}"
{license_comment}license_type: {license_type}
last_upgrade_date {{
year: {year}
month: {month}
day: {day}
}}
}} """
def get_metadata_date(): """Return last_upgrade_date in METADATA or today.""" # When applied to existing directories to normalize METADATA, # we don't want to change the last_upgrade_date.
year, month, day = "", "", "" if os.path.exists("METADATA"): with open("METADATA", "r") as inf: for line in inf:
match = YMD_MATCHER.match(line) if match: if match.group(1) == "year":
year = match.group(2) elif match.group(1) == "month":
month = match.group(2) elif match.group(1) == "day":
day = match.group(2) else:
match = YMD_LINE_MATCHER.match(line) if match:
year, month, day = match.group(1), match.group(2), match.group(3) if year and month and day:
print("### Reuse date in METADATA:", year, month, day) return int(year), int(month), int(day)
today = datetime.date.today() return today.year, today.month, today.day
def add_metadata(name, version, description, license_group, multi_license): """Update or add METADATA file.""" if os.path.exists("METADATA"):
print("### Updating METADATA") else:
print("### Adding METADATA")
year, month, day = get_metadata_date()
license_comment = "" if multi_license:
license_comment = MULTI_LICENSE_COMMENT with open("METADATA", "w") as outf:
outf.write(METADATA_CONTENT.format(
name=name, description=description, version=version,
license_comment=license_comment, license_type=license_group, year=year, month=month, day=day))
def grep_license_keyword(license_file): """Find familiar patterns in a file and return the type.""" with open(license_file, "r") as input_file: for line in input_file: if APACHE_MATCHER.match(line): return License(LicenseType.APACHE2, LicenseGroup.NOTICE, license_file) if BOOST_MATCHER.match(line): return License(LicenseType.BOOST, LicenseGroup.NOTICE, license_file) if MIT_MATCHER.match(line): return License(LicenseType.MIT, LicenseGroup.NOTICE, license_file) if BSD_MATCHER.match(line): return License(LicenseType.BSD_LIKE, LicenseGroup.NOTICE, license_file) if MPL_MATCHER.match(line): return License(LicenseType.MPL, LicenseGroup.RECIPROCAL, license_file) if UNLICENSE_MATCHER.match(line): return License(LicenseType.UNLICENSE, LicenseGroup.PERMISSIVE, license_file) if ZERO_BSD_MATCHER.match(line): return License(LicenseType.ZERO_BSD, LicenseGroup.PERMISSIVE, license_file) if ZLIB_MATCHER.match(line): return License(LicenseType.ZLIB, LicenseGroup.NOTICE, license_file)
print("ERROR: cannot decide license type in", license_file, "assume BSD_LIKE") return License(LicenseType.BSD_LIKE, LicenseGroup.NOTICE, license_file)
class LicenseType(enum.IntEnum): """A type of license.
An IntEnum is used to be able to sort by preference. This is mainly the case for dual-licensed Apache/MIT code, for which we prefer the Apache license.
The enum name is used to generate the corresponding MODULE_LICENSE_* file. """
APACHE2 = 1
MIT = 2
BSD_LIKE = 3
ISC = 4
MPL = 5
ZERO_BSD = 6
UNLICENSE = 7
ZLIB = 8
BOOST = 9
class LicenseGroup(enum.Enum): """A group of license as defined by go/thirdpartylicenses#types
Note, go/thirdpartylicenses#types calls them "types". But LicenseType was
already taken so this script calls them groups. """
RESTRICTED = 1
RESTRICTED_IF_STATICALLY_LINKED = 2
RECIPROCAL = 3
NOTICE = 4
PERMISSIVE = 5
BY_EXCEPTION_ONLY = 6
def decide_license_type(cargo_license): """Check LICENSE* files to determine the license type.
Returns: A list of Licenses. The first element is the license we prefer. """ # Most crates.io packages have both APACHE and MIT. # Some crate like time-macros-impl uses lower case names like LICENSE-Apache.
licenses = []
license_file = None for license_file in glob.glob("license*") + glob.glob("LICENSE*") + glob.glob("COPYING*") + glob.glob("UNLICENSE*"):
lowered_name = os.path.splitext(license_file.lower())[0] if lowered_name == "license-apache":
licenses.append(License(LicenseType.APACHE2, LicenseGroup.NOTICE, license_file)) elif lowered_name == "license-boost":
licenses.append(License(LicenseType.BOOST, LicenseGroup.NOTICE, license_file)) elif lowered_name == "license-bsd":
licenses.append(License(LicenseType.BSD_LIKE, LicenseGroup.NOTICE, license_file)) elif lowered_name == "license-mit":
licenses.append(License(LicenseType.MIT, LicenseGroup.NOTICE, license_file)) elif lowered_name == "license-0bsd":
licenses.append(License(LicenseType.ZERO_BSD, LicenseGroup.PERMISSIVE, license_file)) elif lowered_name == "license-zlib":
licenses.append(License(LicenseType.ZLIB, LicenseGroup.NOTICE, license_file)) elif lowered_name == "unlicense":
licenses.append(License(LicenseType.UNLICENSE, LicenseGroup.PERMISSIVE, license_file)) if licenses:
licenses.sort(key=lambda l: l.type) return licenses ifnot license_file: raise FileNotFoundError("No license file has been found.") # There is a LICENSE* or COPYING* file, use cargo_license found in # Cargo.toml. if"Apache"in cargo_license: return [License(LicenseType.APACHE2, LicenseGroup.NOTICE, license_file)] if"BSL"in cargo_license: return [License(LicenseType.BOOST, LicenseGroup.NOTICE, license_file)] if"MIT"in cargo_license: return [License(LicenseType.MIT, LicenseGroup.NOTICE, license_file)] if"0BSD"in cargo_license: return [License(LicenseType.ZERO_BSD, LicenseGroup.PERMISSIVE, license_file)] if"BSD"in cargo_license: return [License(LicenseType.BSD_LIKE, LicenseGroup.NOTICE, license_file)] if"ISC"in cargo_license: return [License(LicenseType.ISC, LicenseGroup.NOTICE, license_file)] if"MPL"in cargo_license: return [License(LicenseType.MPL, LicenseGroup.RECIPROCAL, license_file)] if"Unlicense"in cargo_license: return [License(LicenseType.UNLICENSE, LicenseGroup.PERMISSIVE, license_file)] if"Zlib"in cargo_license: return [License(LicenseType.ZLIB, LicenseGroup.NOTICE, license_file)] return [grep_license_keyword(license_file)]
def add_notice(): ifnot os.path.exists("NOTICE"): if os.path.exists("LICENSE"):
os.symlink("LICENSE", "NOTICE")
print("Created link from NOTICE to LICENSE") else:
print("ERROR: missing NOTICE and LICENSE")
def check_license_link(target): """Check the LICENSE link, must bet the given target.""" ifnot os.path.islink("LICENSE"):
print("ERROR: LICENSE file is not a link") return
found_target = os.readlink("LICENSE") if target != found_target and found_target != "LICENSE.txt":
print("ERROR: found LICENSE link to", found_target, "but expected", target)
def add_license(target): """Add LICENSE link to give target.""" if os.path.exists("LICENSE"): if os.path.islink("LICENSE"):
check_license_link(target) else:
print("NOTE: found LICENSE and it is not a link.") return
print("### Creating LICENSE link to", target)
os.symlink(target, "LICENSE")
def add_module_license(license_type): """Touch MODULE_LICENSE_type file.""" # Do not change existing MODULE_* files. for suffix in ["MIT", "APACHE", "APACHE2", "BSD_LIKE", "MPL", "0BSD", "UNLICENSE", "ZLIB", "BOOST"]:
module_file = "MODULE_LICENSE_" + suffix if os.path.exists(module_file): if license_type.name != suffix: raise Exception("Found unexpected license " + module_file) return
module_file = "MODULE_LICENSE_" + license_type.name.upper()
pathlib.Path(module_file).touch()
print("### Touched", module_file)
def found_line(file_name, line): """Returns true if the given line is found in a file.""" with open(file_name, "r") as input_file: return line in input_file
def add_owners(): """Create or append OWNERS with the default owner line.""" # Existing OWNERS file might contain more than the default owners. # Only append missing default owners to existing OWNERS. if os.path.isfile("OWNERS"): if found_line("OWNERS", DEFAULT_OWNERS):
print("### No change to OWNERS, which has already default owners.") return else:
print("### Append default owners to OWNERS")
mode = "a" else:
print("### Creating OWNERS with default owners")
mode = "w" with open("OWNERS", mode) as outf:
outf.write(DEFAULT_OWNERS)
def toml2json(line): """Convert a quoted toml string to a json quoted string for METADATA.""" if line.startswith("\"\"\""): return"\"()\""# cannot handle broken multi-line description # TOML string escapes: \b \t \n \f \r \" \\ (no unicode escape)
line = line[1:-1].replace("\\\\", "\n").replace("\\b", "")
line = line.replace("\\t", " ").replace("\\n", " ").replace("\\f", " ")
line = line.replace("\\r", "").replace("\\\"", "\"").replace("\n", "\\") # replace a unicode quotation mark, used in the libloading crate
line = line.replace("’", "'") # strip and escape single quotes return json.dumps(line.strip()).replace("'", "\\'")
def parse_cargo_toml(cargo): """get name, version, description, license string from Cargo.toml."""
name = ""
version = ""
description = ""
cargo_license = "" with open(cargo, "r") as toml: for line in toml: ifnot name and NAME_MATCHER.match(line):
name = NAME_MATCHER.match(line).group(1) elifnot version and VERSION_MATCHER.match(line):
version = VERSION_MATCHER.match(line).group(1) elifnot description and DESCRIPTION_MATCHER.match(line):
description = toml2json(DESCRIPTION_MATCHER.match(line).group(1)) elifnot cargo_license and LICENSE_MATCHER.match(line):
cargo_license = LICENSE_MATCHER.match(line).group(1) if name and version and description and cargo_license: break return name, version, description, cargo_license
def main(): """Add 3rd party review files."""
cargo = "Cargo.toml" ifnot os.path.isfile(cargo):
print("ERROR: ", cargo, "is not found") return ifnot os.access(cargo, os.R_OK):
print("ERROR: ", cargo, "is not readable") return
name, version, description, cargo_license = parse_cargo_toml(cargo) ifnot name ornot version ornot description:
print("ERROR: Cannot find name, version, or description in", cargo) return
print("### Cargo.toml license:", cargo_license)
licenses = decide_license_type(cargo_license)
preferred_license = licenses[0]
add_metadata(name, version, description, preferred_license.group.name, len(licenses) > 1)
add_owners()
add_license(preferred_license.filename)
add_module_license(preferred_license.type) # It is unclear yet if a NOTICE file is required. # add_notice()
if __name__ == "__main__":
main()
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-26)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.