| #!/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 importlib.util |
| import logging |
| import re |
| from enum import Enum |
| from pathlib import Path |
| |
| from cmd_utils import REPO_ROOT, Sh, init_log |
| |
| DATA_PY = REPO_ROOT / "ci" / "jenkins" / "data.py" |
| |
| |
| def load_files_to_stash(): |
| """Load the files_to_stash dict from ci/jenkins/data.py.""" |
| spec = importlib.util.spec_from_file_location("data", DATA_PY) |
| mod = importlib.util.module_from_spec(spec) |
| spec.loader.exec_module(mod) |
| return mod.files_to_stash |
| |
| |
| def resolve_bundles(bundle_names: list[str]) -> list[str]: |
| """Resolve a list of bundle names to a flat list of file paths.""" |
| files_to_stash = load_files_to_stash() |
| items = [] |
| for name in bundle_names: |
| if name not in files_to_stash: |
| known = list(files_to_stash.keys()) |
| logging.error(f"Unknown bundle '{name}'. Known bundles: {known}") |
| raise SystemExit(1) |
| items.extend(files_to_stash[name]) |
| return items |
| |
| |
| RETRY_SCRIPT = REPO_ROOT / "ci" / "scripts" / "jenkins" / "retry.sh" |
| S3_DOWNLOAD_REGEX = re.compile(r"download: s3://.* to (.*)") |
| SH = Sh() |
| |
| |
| class Action(Enum): |
| UPLOAD = 1 |
| DOWNLOAD = 2 |
| |
| |
| def show_md5(item: str) -> None: |
| if not Path(item).is_dir(): |
| sh.run(f"md5sum {item}") |
| |
| |
| def parse_output_files(stdout: str) -> list[str]: |
| """ |
| Grab the list of downloaded files from the output of 'aws s3 cp'. Lines look |
| like: |
| |
| download: s3://some/prefix/a_file.txt to a_file.txt |
| """ |
| files = [] |
| for line in stdout.split("\n"): |
| line = line.strip() |
| if line == "": |
| continue |
| m = S3_DOWNLOAD_REGEX.match(line) |
| if m: |
| files.append(m.groups()[0]) |
| |
| return files |
| |
| |
| def chmod(files: list[str]) -> None: |
| """ |
| S3 has no concept of file permissions so add them back in here to every file |
| """ |
| # Add execute bit for downloads |
| to_chmod = [str(f) for f in files] |
| logging.info(f"Adding execute bit for files: {to_chmod}") |
| if len(to_chmod) > 0: |
| SH.run(f"chmod +x {' '.join(to_chmod)}") |
| |
| |
| def s3(source: str, destination: str, recursive: bool) -> list[str]: |
| """ |
| Send or download the source to the destination in S3 |
| """ |
| cmd = f". {RETRY_SCRIPT.relative_to(REPO_ROOT)} && retry 3 aws s3 cp --no-progress" |
| |
| if recursive: |
| cmd += " --recursive" |
| |
| cmd += f" {source} {destination}" |
| _, stdout = SH.tee(cmd) |
| return stdout |
| |
| |
| if __name__ == "__main__": |
| init_log() |
| help = "Uploads or downloads files from S3" |
| parser = argparse.ArgumentParser(description=help) |
| parser.add_argument("--action", help="either 'upload' or 'download'", required=True) |
| parser.add_argument("--bucket", help="s3 bucket", required=True) |
| parser.add_argument( |
| "--prefix", help="s3 bucket + tag (e.g. s3://tvm-ci-prod/PR-1234/cpu", required=True |
| ) |
| parser.add_argument("--items", help="files and folders to upload", nargs="+") |
| parser.add_argument( |
| "--bundle", |
| help="bundle name(s) from ci/jenkins/data.py files_to_stash (repeatable)", |
| action="append", |
| dest="bundles", |
| metavar="NAME", |
| ) |
| |
| args = parser.parse_args() |
| |
| if args.items is not None and args.bundles is not None: |
| parser.error("--items and --bundle are mutually exclusive") |
| |
| logging.info(args) |
| |
| sh = Sh() |
| |
| if Path.cwd() != REPO_ROOT: |
| logging.error(f"s3.py can only be executed from the repo root, instead was in {Path.cwd()}") |
| exit(1) |
| |
| prefix = args.prefix.strip("/") |
| s3_path = f"s3://{args.bucket}/{prefix}" |
| logging.info(f"Using s3 path: {s3_path}") |
| |
| if args.action == "upload": |
| action = Action.UPLOAD |
| elif args.action == "download": |
| action = Action.DOWNLOAD |
| else: |
| logging.error(f"Unsupported action: {args.action}") |
| exit(1) |
| |
| if args.bundles is not None: |
| items = resolve_bundles(args.bundles) |
| elif args.items is not None: |
| items = args.items |
| else: |
| if args.action == "upload": |
| logging.error("Cannot upload without --items or --bundle") |
| exit(1) |
| else: |
| # Download the whole prefix |
| items = ["."] |
| |
| for item in items: |
| if action == Action.DOWNLOAD: |
| source = s3_path |
| recursive = True |
| if item != ".": |
| source = s3_path + "/" + item |
| recursive = False |
| try: |
| stdout = s3(source=source, destination=item, recursive=recursive) |
| except Exception: |
| # Optional artifacts (e.g. per-backend device runtime DSOs) may not |
| # exist in S3 when the build config didn't produce them. Skip silently. |
| logging.warning(f"Download failed for {item}, skipping (may be optional)") |
| continue |
| files = parse_output_files(stdout) |
| chmod(files) |
| for file in files: |
| # Show md5 after downloading |
| show_md5(file) |
| elif action == Action.UPLOAD: |
| if not Path(item).exists(): |
| logging.warning(f"The path doesn't exist: {item}") |
| continue |
| show_md5(item) |
| if Path(item).is_dir(): |
| if len(list(Path(item).glob("**/*"))) == 0: |
| raise RuntimeError(f"Cannot upload empty folder with name: {item}") |
| s3(item, s3_path + "/" + item, recursive=Path(item).is_dir()) |