blob: c05a3cf843760ed765dc3ab496420e0aa1517788 [file] [log] [blame]
#
# Copyright (C) 2022 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.
#
"""Helper functions for reading and writing .zip and .tar.bz2 archives."""
import os
from pathlib import Path
import shutil
import subprocess
from typing import List
def make_bztar(base_name: Path, root_dir: Path, base_dir: Path) -> None:
"""Create a compressed tarball.
Arguments have the same name and meaning as shutil.make_archive.
Args:
base_name: Base name of archive to create. ".tar.bz2" will be appended.
root_dir: Directory that's the root of the archive.
base_dir: Directory relative to root_dir to archive.
"""
if not root_dir.is_dir():
raise RuntimeError(f"Not a directory: {root_dir}")
if not (root_dir / base_dir).is_dir():
raise RuntimeError(f"Not a directory: {root_dir}/{base_dir}")
if os.name == "nt":
shutil.make_archive(
str(base_name),
"bztar",
str(root_dir),
str(base_dir),
)
else:
subprocess.check_call(
[
"tar",
"-j"
if shutil.which("pbzip2") is None
else "--use-compress-prog=pbzip2",
"-cf",
str(base_name.with_suffix(".tar.bz2")),
"-C",
str(root_dir),
str(base_dir),
]
)
def make_xztar(
base_name: Path, root_dir: Path, base_dir: Path, preserve_symlinks: bool
) -> Path:
"""Create a xz-compressed tarball.
Arguments have the same name and meaning as shutil.make_archive.
Args:
base_name: Base name of archive to create. ".tar.xz" will be appended.
root_dir: Directory that's the root of the archive.
base_dir: Directory relative to root_dir to archive.
"""
if not root_dir.is_dir():
raise RuntimeError(f"Not a directory: {root_dir}")
if not (root_dir / base_dir).is_dir():
raise RuntimeError(f"Not a directory: {root_dir}/{base_dir}")
xz_file = base_name.with_suffix(".tar.xz")
cmd = ["c:/windows/system32/tar.exe"] if os.name == "nt" else ["tar"]
if not preserve_symlinks:
cmd.append("--dereference")
cmd.extend(
[
"-J",
"-cf",
str(xz_file),
"-C",
str(root_dir),
str(base_dir),
]
)
env = dict(os.environ)
env["XZ_OPT"] = "-T0"
subprocess.check_call(cmd, env=env)
return xz_file
# For (un)zipping archives on Unix-like systems, the "zip" and "unzip" commands
# are pretty universally available.
#
# For Windows, the situation is more complicated. After trying and rejecting
# several options, the somewhat surprising best choice is the "tar"
# command, which is available in Windows since 2018:
# https://docs.microsoft.com/en-us/virtualization/community/team-blog/2017/20171219-tar-and-curl-come-to-windows
# Note that this is bsdtar, which has slightly different command-line flags
# than GNU tar.
#
# For the record, here are other options, and why they didn't work:
#
# - Python's built-in shutil.unpack_archive uses the "zipfile" module, which
# does not restore permissions, including the executable bit, when
# unzipping. https://bugs.python.org/issue15795
#
# - 7-zip is popular and works on a wide range of Windows versions,
# but it is not guaranteed to be available, and is, in fact, not
# available on our Windows build machines.
#
# - Expand-Archive in PowerShell results in modification times in the future
# for NDK .zip files, possibly due to not handling time zones correctly.
#
# For more information, see https://superuser.com/questions/1314420/how-to-unzip-a-file-using-the-cmd
#
# See also the following changes:
# - 7-zip: https://android-review.googlesource.com/c/platform/ndk/+/1963599
# - PowerShell: https://android-review.googlesource.com/c/platform/ndk/+/1965510
# - Tar: https://android-review.googlesource.com/c/platform/ndk/+/1967235
def make_zip(
base_name: Path, root_dir: Path, paths: List[str], preserve_symlinks: bool
) -> Path:
"""Creates a zip package for distribution.
Args:
base_name: Path (without extension) to the output archive.
root_dir: Path to the directory from which to perform the packaging
(identical to tar's -C).
paths: Paths to files and directories to package, relative to root_dir.
preserve_symlinks: Whether to preserve or flatten symlinks. Should be
false when creating packages for Windows, but otherwise true.
"""
if not root_dir.is_dir():
raise RuntimeError(f"Not a directory: {root_dir}")
cwd = os.getcwd()
zip_file = base_name.with_suffix(".zip")
if zip_file.exists():
zip_file.unlink()
# See comment above regarding .zip files on Windows.
if os.name == "nt":
# Explicit path, to avoid conflict with Cygwin.
args = ["c:/windows/system32/tar.exe", "-a"]
if not preserve_symlinks:
args.append("-L")
args.extend(["-cf", str(zip_file)])
else:
args = ["zip", "-9qr", str(zip_file)]
if preserve_symlinks:
args.append("--symlinks")
args.extend(paths)
os.chdir(root_dir)
try:
subprocess.check_call(args)
return zip_file
finally:
os.chdir(cwd)
def unzip(zip_file: Path, dest_dir: Path) -> None:
"""Unzip zip_file into dest_dir."""
if not zip_file.is_file() or zip_file.suffix != ".zip":
raise RuntimeError(f"Not a .zip file: {zip_file}")
if not dest_dir.is_dir():
raise RuntimeError(f"Not a directory: {dest_dir}")
# See comment above regarding .zip files on Windows.
if os.name == "nt":
subprocess.check_call(
[
# Explicit path, to avoid conflict with Cygwin.
"c:/windows/system32/tar.exe",
"xf",
str(zip_file),
"-C",
str(dest_dir),
]
)
else:
# Unzip seems to be pretty universally available on posix systems.
subprocess.check_call(["unzip", "-qq", str(zip_file), "-d", str(dest_dir)])