CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
274 lines
9.7 KiB
Python
274 lines
9.7 KiB
Python
# Copyright Advanced Micro Devices, Inc.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
from typing import Generator, Sequence
|
|
|
|
import os
|
|
from pathlib import Path, PurePosixPath
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import sys
|
|
|
|
from .os_util import rmtree_with_retry
|
|
|
|
_IS_WINDOWS = platform.system() == "Windows"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# File copy strategies for copy_to.
|
|
#
|
|
# Three mutually exclusive strategies for placing a regular file at destpath:
|
|
#
|
|
# 1. _hardlink_or_copy_from_source: hardlink dest to src (shares inode with
|
|
# source), falling back to copy on failure (e.g. cross-device).
|
|
# Used by default to avoid redundant copies within a build tree.
|
|
#
|
|
# 2. _copy_preserving_hardlink_groups: copy file content (new inode), but
|
|
# re-hardlink files that shared an inode in the source so they still
|
|
# share an inode in the destination. Used for artifact populate where
|
|
# we must break sharing with the source tree but preserve internal
|
|
# hardlink structure (e.g. libfoo.so.1 <-> libfoo.so.1.0.0).
|
|
# Not available on Windows (st_dev/st_ino unreliable).
|
|
#
|
|
# 3. _plain_copy: shutil.copy2, no inode tracking. Used on Windows with
|
|
# always_copy, where we can't reliably detect hardlink groups.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _hardlink_or_copy_from_source(src: str, destpath: Path, verbose: bool) -> None:
|
|
"""Hardlink destpath to src, falling back to copy on failure."""
|
|
try:
|
|
if verbose:
|
|
print(f"hardlink {src} -> {destpath}", file=sys.stderr, end="")
|
|
os.link(src, destpath, follow_symlinks=False)
|
|
except OSError:
|
|
if verbose:
|
|
print(" (falling back to copy) ", file=sys.stderr, end="")
|
|
_plain_copy(src, destpath, verbose)
|
|
|
|
|
|
def _copy_preserving_hardlink_groups(
|
|
src: str,
|
|
destpath: Path,
|
|
verbose: bool,
|
|
copied_inodes: dict[tuple[int, int], Path],
|
|
) -> None:
|
|
"""Copy file, but hardlink to a previous copy if the source inode matches.
|
|
|
|
This gives tar-like behavior: files hardlinked together in the source
|
|
remain hardlinked together in the destination, but no destination file
|
|
shares an inode with the source.
|
|
"""
|
|
src_stat = os.stat(src)
|
|
inode_key = (src_stat.st_dev, src_stat.st_ino)
|
|
prev_dest = copied_inodes.get(inode_key)
|
|
if prev_dest is not None:
|
|
if verbose:
|
|
print(
|
|
f"hardlink (internal) {prev_dest} -> {destpath}",
|
|
file=sys.stderr,
|
|
end="",
|
|
)
|
|
os.link(prev_dest, destpath)
|
|
return
|
|
# First time seeing this inode: copy and record.
|
|
if verbose:
|
|
print(f"copy {src} -> {destpath}", file=sys.stderr, end="")
|
|
shutil.copy2(src, destpath, follow_symlinks=False)
|
|
copied_inodes[inode_key] = destpath
|
|
|
|
|
|
def _plain_copy(src: str, destpath: Path, verbose: bool) -> None:
|
|
if verbose:
|
|
print(f"copy {src} -> {destpath}", file=sys.stderr, end="")
|
|
shutil.copy2(src, destpath, follow_symlinks=False)
|
|
|
|
|
|
class RecursiveGlobPattern:
|
|
def __init__(self, glob: str):
|
|
self.glob = glob
|
|
pattern = f"^{re.escape(glob)}$"
|
|
# Intermediate recursive directory match.
|
|
pattern = pattern.replace("/\\*\\*/", "/(.*/)?")
|
|
# First segment recursive directory match.
|
|
pattern = pattern.replace("^\\*\\*/", "^(.*/)?")
|
|
# Last segment recursive directory match.
|
|
pattern = pattern.replace("/\\*\\*$", "(/.*)?$")
|
|
# Intra-segment * match.
|
|
pattern = pattern.replace("\\*", "[^/]*")
|
|
# Intra-segment ? match.
|
|
pattern = pattern.replace("\\?", "[^/]*")
|
|
self.pattern = re.compile(pattern)
|
|
|
|
def matches(self, relpath: str, direntry: os.DirEntry[str]) -> bool:
|
|
m = self.pattern.match(relpath)
|
|
return True if m else False
|
|
|
|
|
|
class MatchPredicate:
|
|
def __init__(
|
|
self,
|
|
includes: Sequence[str] = (),
|
|
excludes: Sequence[str] = (),
|
|
force_includes: Sequence[str] = (),
|
|
):
|
|
self.includes = [RecursiveGlobPattern(p) for p in includes]
|
|
self.excludes = [RecursiveGlobPattern(p) for p in excludes]
|
|
self.force_includes = [RecursiveGlobPattern(p) for p in force_includes]
|
|
|
|
def matches(self, match_path: str, direntry: os.DirEntry[str]):
|
|
includes = self.includes
|
|
excludes = self.excludes
|
|
force_includes = self.force_includes
|
|
if force_includes:
|
|
for force_include in force_includes:
|
|
if force_include.matches(match_path, direntry):
|
|
return True
|
|
if includes:
|
|
for include in includes:
|
|
if include.matches(match_path, direntry):
|
|
break
|
|
else:
|
|
return False
|
|
for exclude in excludes:
|
|
if exclude.matches(match_path, direntry):
|
|
return False
|
|
return True
|
|
|
|
|
|
class PatternMatcher:
|
|
|
|
def __init__(
|
|
self,
|
|
includes: Sequence[str] = (),
|
|
excludes: Sequence[str] = (),
|
|
force_includes: Sequence[str] = (),
|
|
):
|
|
self.predicate = MatchPredicate(includes, excludes, force_includes)
|
|
# Dictionary of relative posix-style path to DirEntry.
|
|
# Last relative path to entry.
|
|
self.all: dict[str, os.DirEntry[str]] = {}
|
|
|
|
def add_basedir(self, basedir: Path):
|
|
all = self.all
|
|
basedir = basedir.absolute()
|
|
|
|
# Using scandir and being judicious about path concatenation/conversion
|
|
# (versus using walk) is on the order of 10-50x faster. This is still
|
|
# about 10x slower than an `ls -R` but gets us down to tens of
|
|
# milliseconds for an LLVM install sized tree, which is acceptable.
|
|
def scan_children(rootpath: str, prefix: str):
|
|
with os.scandir(rootpath) as it:
|
|
for entry in it:
|
|
if entry.is_dir(follow_symlinks=False):
|
|
relpath = f"{prefix}{entry.name}"
|
|
new_rootpath = os.path.join(rootpath, entry.name)
|
|
all[relpath] = entry
|
|
scan_children(new_rootpath, f"{relpath}/")
|
|
else:
|
|
relpath = f"{prefix}{entry.name}"
|
|
all[relpath] = entry
|
|
|
|
scan_children(basedir, "")
|
|
|
|
def add_entry(self, relpath: str, direntry: os.DirEntry):
|
|
self.all[relpath] = direntry
|
|
|
|
def matches(self) -> Generator[tuple[str, os.DirEntry[str]], None, None]:
|
|
for match_path, direntry in self.all.items():
|
|
if self.predicate.matches(match_path, direntry):
|
|
yield match_path, direntry
|
|
|
|
def copy_to(
|
|
self,
|
|
*,
|
|
destdir: Path,
|
|
destprefix: str = "",
|
|
verbose: bool = False,
|
|
always_copy: bool = False,
|
|
remove_dest: bool = True,
|
|
):
|
|
if remove_dest and destdir.exists():
|
|
rmtree_with_retry(destdir, verbose=verbose)
|
|
destdir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Inode tracking for _copy_preserving_hardlink_groups.
|
|
copied_inodes: dict[tuple[int, int], Path] = {}
|
|
|
|
for relpath, direntry in self.matches():
|
|
try:
|
|
destpath = destdir / PurePosixPath(destprefix + relpath)
|
|
if direntry.is_dir() and not direntry.is_symlink():
|
|
if verbose:
|
|
print(f"mkdir {destpath}", file=sys.stderr, end="")
|
|
destpath.mkdir(parents=True, exist_ok=True)
|
|
elif direntry.is_symlink():
|
|
self._copy_symlink(direntry, destpath, remove_dest, verbose)
|
|
else:
|
|
self._copy_regular_file(
|
|
direntry,
|
|
destpath,
|
|
always_copy,
|
|
remove_dest,
|
|
verbose,
|
|
copied_inodes,
|
|
)
|
|
finally:
|
|
if verbose:
|
|
print("", file=sys.stderr)
|
|
|
|
@staticmethod
|
|
def _copy_symlink(
|
|
direntry: os.DirEntry[str],
|
|
destpath: Path,
|
|
remove_dest: bool,
|
|
verbose: bool,
|
|
) -> None:
|
|
if not remove_dest and (destpath.exists() or destpath.is_symlink()):
|
|
os.unlink(destpath)
|
|
targetpath = os.readlink(direntry.path)
|
|
if verbose:
|
|
print(f"symlink {targetpath} -> {destpath}", file=sys.stderr, end="")
|
|
destpath.parent.mkdir(parents=True, exist_ok=True)
|
|
os.symlink(targetpath, destpath)
|
|
|
|
@staticmethod
|
|
def _copy_regular_file(
|
|
direntry: os.DirEntry[str],
|
|
destpath: Path,
|
|
always_copy: bool,
|
|
remove_dest: bool,
|
|
verbose: bool,
|
|
copied_inodes: dict[tuple[int, int], Path],
|
|
) -> None:
|
|
# When hardlinking to source, another process may have already
|
|
# created the link. On Windows files in use can't be removed, so
|
|
# detect this and skip.
|
|
if (
|
|
not always_copy
|
|
and destpath.exists()
|
|
and os.stat(destpath).st_ino == os.stat(direntry.path).st_ino
|
|
):
|
|
if verbose:
|
|
print(
|
|
f"skipping (already hardlinked) {direntry.path}",
|
|
file=sys.stderr,
|
|
end="",
|
|
)
|
|
return
|
|
|
|
if not remove_dest and (destpath.exists() or destpath.is_symlink()):
|
|
os.unlink(destpath)
|
|
destpath.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Dispatch to the appropriate strategy.
|
|
if not always_copy:
|
|
_hardlink_or_copy_from_source(direntry.path, destpath, verbose)
|
|
elif _IS_WINDOWS:
|
|
_plain_copy(direntry.path, destpath, verbose)
|
|
else:
|
|
_copy_preserving_hardlink_groups(
|
|
direntry.path, destpath, verbose, copied_inodes
|
|
)
|