Skip to content

Commit

Permalink
build tooling: simplify and improve caching
Browse files Browse the repository at this point in the history
  • Loading branch information
supermihi committed Mar 17, 2024
1 parent c225853 commit b9e0848
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 196 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
uses: actions/cache@v4
with:
path: build
key: taglib-${{ matrix.os }}-${{ hashFiles('build_taglib.py') }}
key: taglib-${{ matrix.os }}-${{ hashFiles('build_native_taglib.py') }}
- name: build binary wheels
uses: pypa/[email protected]
- name: upload wheels
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ and also development tools for Python.
On Ubuntu, Mint and other Debian-Based distributions, install
the `libtag1-dev` and `python-dev` packages. On Fedora and friends, these are called `taglib-devel` and `python-devel`, respectively. On a Mac, use HomeBrew to install the `taglib` package. For Windows, see below.

As an alternative, run `python build_taglib.py` in this directory to
automatically download and build the latest Taglib version into the `build` subdirectory (also works on Windows). This requires Python and a
suitable compiler to be installed; specific instructions are beyond the
As an alternative, run `python build_native_taglib.py` in this directory to
automatically download and build the latest Taglib version into the `lib/taglib-cpp` subdirectory (also works on
Windows).

This requires Python and a suitable compiler to be installed; specific instructions are beyond the
scope of this README.

### Linux: Distribution-Specific Packages
Expand Down Expand Up @@ -98,7 +100,7 @@ Then:

- open the VS native tools command prompt
- navigate to the _pytaglib_ repository
- run `python build_taglib.py` which will download and build the latest official TagLib release
- run `python build_native_taglib.py` which will download and build the latest official TagLib release
- run `python setup.py install`

## Contact
Expand Down
185 changes: 185 additions & 0 deletions build_native_taglib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import hashlib
import os
import platform
import shutil
import subprocess
import sys
import tarfile
import urllib.request
from argparse import ArgumentParser
from dataclasses import dataclass
from pathlib import Path

taglib_version = "2.0"
taglib_url = f"https://github.com/taglib/taglib/archive/refs/tags/v{taglib_version}.tar.gz"
taglib_sha256sum = "e36ea877a6370810b97d84cf8f72b1e4ed205149ab3ac8232d44c850f38a2859"

utfcpp_version = "4.0.5"
utfcpp_url = f"https://github.com/nemtrif/utfcpp/archive/refs/tags/v{utfcpp_version}.tar.gz"

root = Path(__file__).resolve().parent

system = platform.system()


def run_script():
config = get_config()

_download(taglib_url, config.taglib_tarball, taglib_sha256sum)
_download(utfcpp_url, config.utfcpp_tarball)

if config.force or not config.taglib_extract_dir.exists():
_del_if_exists(config.taglib_extract_dir)
_del_if_exists(config.utfcpp_extract_dir)
_extract(config.taglib_tarball, config.taglib_extract_dir)
_extract(config.utfcpp_tarball, config.utfcpp_extract_dir)
shutil.copytree(config.utfcpp_extract_dir, config.taglib_extract_dir / "3rdparty" / "utfcpp",
dirs_exist_ok=True)

if config.force or not config.taglib_install_dir.exists():
_del_if_exists(config.taglib_install_dir)
cmake_config(config.taglib_extract_dir, config.taglib_install_dir)
cmake_build(config.taglib_extract_dir)

_del_if_exists(config.target_dir)
shutil.copytree(config.taglib_install_dir, config.target_dir)


@dataclass
class Configuration:
target_dir: Path
cache_dir: Path
force: bool
platform_id: str

@property
def taglib_tarball(self):
return self.cache_dir / f"taglib-{taglib_version}.tar.gz"

@property
def utfcpp_tarball(self):
return self.cache_dir / f"utfcpp-{utfcpp_version}.tar.gz"

@property
def platform_dir(self):
return self.cache_dir / self.platform_id

@property
def taglib_extract_dir(self):
return self.platform_dir / f"taglib-{taglib_version}"

@property
def utfcpp_extract_dir(self):
return self.platform_dir / f"utfcpp-{utfcpp_version}"

@property
def taglib_install_dir(self):
return self.platform_dir / "taglib-install"


def get_config() -> Configuration:
parser = ArgumentParser(description="helper to download and build C++ TagLib")
parser.add_argument(
"--target",
help="target directory for TagLib (binaries and headers)",
type=Path,
default=root / "lib" / "taglib-cpp",
)
parser.add_argument(
"--cache",
help="temporary directory for downloads and builds; suitable to be cached in CI",
type=Path,
default=root / "build" / "cache",
)
parser.add_argument("--force", action="store_true", help="fore clean build even if output already exists")
args = parser.parse_args()
return Configuration(target_dir=args.target.resolve(), cache_dir=args.cache.resolve(), force=args.force,
platform_id=get_platform_id())


def _download(url: str, target: Path, sha256sum: str = None):
print(f"downloading {url} ...")
if target.exists():
print("skipping download, file exists")
return
response = urllib.request.urlopen(url)
data = response.read()
target.parent.mkdir(exist_ok=True, parents=True)
target.write_bytes(data)
print("download complete")
if sha256sum is None:
return
the_hash = hashlib.sha256(target.read_bytes()).hexdigest()
if the_hash != taglib_sha256sum:
error = f'checksum of downloaded file ({the_hash}) does not match expected hash ({taglib_sha256sum})'
raise RuntimeError(error)


def get_platform_id():
"""Tries to generate a string that is unique for the C compiler configuration used to build
Python extensions.
Compiler potentially depends on:
- OS
- architecture
- Python implementation (CPython, PyPy)
- Python version (major/minor)
- C library type (uclib for musllinux)
In cibuildwheel, the AUDITWHEEL_PLAT environment variable is used for all of these except
Python version and implementation.
- """
platform_identifier = os.environ.get('AUDITWHEEL_PLAT', f"{system}-{platform.machine()}")
python_identifier = f"{sys.implementation.name}-{sys.version_info[0]}.{sys.version_info[1]}"
return f"{platform_identifier}-{python_identifier}"


def _extract(archive: Path, target: Path):
"""Extracts `archive` into `target`.
"""
print(f"extracting {archive} to {target} ...")
tar = tarfile.open(archive)
tar.extractall(target.parent)


def _del_if_exists(dir: Path):
if dir.exists():
shutil.rmtree(dir)


def _cmake(cwd: Path, *args):
print(f"running cmake {' '.join(args)}")
return subprocess.run(["cmake", *args], cwd=cwd, check=True)


def cmake_config(source_dir: Path, install_dir: Path):
print("running cmake ...")
args = ["-DWITH_ZLIB=OFF"] # todo fix building wheels with zlib support
if system == "Windows":
args += ["-A", "x64"]
elif system == "Linux":
args.append("-DCMAKE_POSITION_INDEPENDENT_CODE=ON")
args.append("-DBUILD_TESTING=OFF")
args.append(f"-DCMAKE_INSTALL_PREFIX={install_dir}")
args.append(f"-DCMAKE_CXX_FLAGS=-I{source_dir / '3rdparty' / 'utfcpp' / 'source'}")
args.append(".")
install_dir.mkdir(exist_ok=True, parents=True)
_cmake(source_dir, *args)


def cmake_build(source_dir: Path):
print("building taglib ...")
build_configuration = "Release"
_cmake(
source_dir,
"--build",
".",
"--config",
build_configuration
)
print("installing cmake ...")
_cmake(source_dir, "--install", ".", "--config", build_configuration)


if __name__ == "__main__":
run_script()
Loading

0 comments on commit b9e0848

Please sign in to comment.