#!/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())