blob: c7c15c850fa6654d930a069bf5d323261208d488 [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.
import argparse
import json
import os
import tomllib
from collections import defaultdict, deque
from dataclasses import dataclass
from pathlib import Path
SCRIPT_PATH = Path(__file__).resolve()
PROJECT_DIR = SCRIPT_PATH.parents[3]
PUBLISH_GLOBS = (
"core/Cargo.toml",
"core/core/Cargo.toml",
"core/layers/*/Cargo.toml",
"core/services/*/Cargo.toml",
"integrations/*/Cargo.toml",
)
@dataclass(frozen=True)
class Package:
manifest_path: Path
manifest_dir: Path
path: str
def discover_publishable_packages(project_dir: Path) -> dict[Path, Package]:
packages: dict[Path, Package] = {}
for pattern in PUBLISH_GLOBS:
for manifest_path in sorted(project_dir.glob(pattern)):
with manifest_path.open("rb") as fp:
manifest = tomllib.load(fp)
package = manifest.get("package")
if not package or package.get("publish") is False:
continue
manifest_dir = manifest_path.parent.resolve()
packages[manifest_dir] = Package(
manifest_path=manifest_path.resolve(),
manifest_dir=manifest_dir,
path=manifest_dir.relative_to(project_dir).as_posix(),
)
return packages
def iter_local_dependencies(manifest: dict, manifest_dir: Path) -> list[Path]:
dependencies: list[Path] = []
def visit_table(table: dict | None) -> None:
if not isinstance(table, dict):
return
for dependency in table.values():
if not isinstance(dependency, dict):
continue
path = dependency.get("path")
if not isinstance(path, str):
continue
dependencies.append((manifest_dir / path).resolve())
for name in ("dependencies", "build-dependencies"):
visit_table(manifest.get(name))
for target in manifest.get("target", {}).values():
if not isinstance(target, dict):
continue
for name in ("dependencies", "build-dependencies"):
visit_table(target.get(name))
return dependencies
def plan(project_dir: Path = PROJECT_DIR) -> list[str]:
project_dir = project_dir.resolve()
packages = discover_publishable_packages(project_dir)
graph: dict[Path, set[Path]] = defaultdict(set)
indegree = {manifest_dir: 0 for manifest_dir in packages}
for manifest_dir, package in packages.items():
with package.manifest_path.open("rb") as fp:
manifest = tomllib.load(fp)
for dependency_dir in iter_local_dependencies(manifest, manifest_dir):
if dependency_dir not in packages:
continue
if manifest_dir in graph[dependency_dir]:
continue
graph[dependency_dir].add(manifest_dir)
indegree[manifest_dir] += 1
queue = deque(
sorted(
(manifest_dir for manifest_dir, degree in indegree.items() if degree == 0),
key=lambda manifest_dir: packages[manifest_dir].path,
)
)
ordered: list[str] = []
while queue:
manifest_dir = queue.popleft()
ordered.append(packages[manifest_dir].path)
for dependent in sorted(
graph[manifest_dir], key=lambda dependent: packages[dependent].path
):
indegree[dependent] -= 1
if indegree[dependent] == 0:
queue.append(dependent)
if len(ordered) != len(packages):
raise RuntimeError("failed to resolve publish order for Rust crates")
return ordered
def write_github_output(packages: list[str]) -> None:
github_output = os.environ.get("GITHUB_OUTPUT")
if not github_output:
raise RuntimeError("GITHUB_OUTPUT is not set")
with Path(github_output).open("a", encoding="utf-8") as fp:
fp.write(f"packages={json.dumps(packages)}\n")
def main() -> int:
parser = argparse.ArgumentParser(
description="Plan the publish order for Rust crates released from this repository."
)
parser.add_argument(
"--project-dir",
type=Path,
default=PROJECT_DIR,
help="Path to the repository root.",
)
parser.add_argument(
"--github-output",
action="store_true",
help="Write the planned package list to GITHUB_OUTPUT as `packages=<json>`.",
)
args = parser.parse_args()
packages = plan(args.project_dir)
print(json.dumps(packages))
if args.github_output:
write_github_output(packages)
return 0
if __name__ == "__main__":
raise SystemExit(main())