feat: use rust triple instead of python sys module
after this change, cross compilation are supported in theory.
This commit is contained in:
@@ -41,7 +41,7 @@ class App:
|
||||
self.__opts = opts
|
||||
# build essential instances
|
||||
self.__extractor = MetadataExtractor(self.__opts.manifest)
|
||||
self.__ctx = ArtifactContext(self.__extractor)
|
||||
self.__ctx = ArtifactContext(self.__opts, self.__extractor)
|
||||
|
||||
def run(self) -> None:
|
||||
"""Produce the distribution.
|
||||
|
||||
@@ -5,12 +5,13 @@ header copies, library copies and generated CMake/pkg-config texts that the
|
||||
archiver then materializes.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from typing import Iterator
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from re import Pattern, compile
|
||||
from .cli import Cli
|
||||
from .utils import Triple
|
||||
from .metadata import MetadataExtractor, Metadata
|
||||
from .renders.cmake import CMakeProperties, CMakeRender
|
||||
from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender
|
||||
@@ -19,17 +20,24 @@ from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender
|
||||
class ArtifactContext:
|
||||
"""A wrapper of metadata with proper fallback"""
|
||||
|
||||
__opts: Cli
|
||||
__extractor: MetadataExtractor
|
||||
__metadata: Metadata
|
||||
|
||||
def __init__(self, extractor: MetadataExtractor) -> None:
|
||||
def __init__(self, opts: Cli, extractor: MetadataExtractor) -> None:
|
||||
"""Wrap an extractor and eagerly fetch its :class:`Metadata`.
|
||||
|
||||
:param extractor: The extractor to wrap and read from.
|
||||
"""
|
||||
self.__opts = opts
|
||||
self.__extractor = extractor
|
||||
self.__metadata = self.__extractor.get_metadata()
|
||||
|
||||
@property
|
||||
def options(self) -> Cli:
|
||||
"""The wrapped :class:`Cli` holding all command line arguments"""
|
||||
return self.__opts
|
||||
|
||||
@property
|
||||
def extractor(self) -> MetadataExtractor:
|
||||
"""The wrapped :class:`MetadataExtractor`."""
|
||||
@@ -112,20 +120,39 @@ def resolve_include_copy(ctx: ArtifactContext) -> Iterator[FileCopyInfo]:
|
||||
)
|
||||
|
||||
|
||||
def _generate_dll_artifact_name(name: str) -> str:
|
||||
_WINDOWS_ENV_IDENTS: tuple[str, ...] = ("windows", "cygwin")
|
||||
|
||||
_LINUX_ENV_IDENTS: tuple[str, ...] = (
|
||||
"linux",
|
||||
"android",
|
||||
"freebsd",
|
||||
"openbsd",
|
||||
"netbsd",
|
||||
"haiku",
|
||||
"hurd",
|
||||
)
|
||||
|
||||
_MACOS_ENV_IDENTS: tuple[str, ...] = ("macos", "ios", "tvos", "visionos")
|
||||
|
||||
|
||||
def _is_windows_env(triple: Triple) -> bool:
|
||||
return triple.operating_system in _WINDOWS_ENV_IDENTS
|
||||
|
||||
|
||||
def _generate_dll_artifact_name(name: str, triple: Triple) -> str:
|
||||
"""Return the platform-appropriate dynamic-loadable file name for ``name``."""
|
||||
match sys.platform:
|
||||
case "win32" | "cygwin":
|
||||
match triple.operating_system:
|
||||
case x if x in _WINDOWS_ENV_IDENTS:
|
||||
return f"{name}.dll"
|
||||
case "linux" | "android" | "freebsd":
|
||||
case x if x in _LINUX_ENV_IDENTS:
|
||||
return f"lib{name}.so"
|
||||
case "darwin" | "ios":
|
||||
case x if x in _MACOS_ENV_IDENTS:
|
||||
return f"lib{name}.dylib"
|
||||
case _:
|
||||
raise RuntimeError("not supported system")
|
||||
|
||||
|
||||
def _generate_lib_artifact_name(name: str) -> str:
|
||||
def _generate_lib_artifact_name(name: str, _: Triple) -> str:
|
||||
"""Return the Windows import-library file name for ``name``.
|
||||
|
||||
Only meaningful on Windows; returned regardless of platform for use by the
|
||||
@@ -141,15 +168,21 @@ def resolve_lib_copy(ctx: ArtifactContext) -> Iterator[FileCopyInfo]:
|
||||
target_directory = extractor.get_target_directory() / "release"
|
||||
target_name = extractor.get_target_name()
|
||||
|
||||
# get triple info
|
||||
target_triple = extractor.get_host_triple()
|
||||
if ctx.options.target is not None:
|
||||
target_triple = ctx.options.target
|
||||
|
||||
# copy artifact for redist
|
||||
dll_artifact_filename = _generate_dll_artifact_name(target_name)
|
||||
dll_artifact_filename = _generate_dll_artifact_name(target_name, target_triple)
|
||||
yield FileCopyInfo(
|
||||
target_directory / dll_artifact_filename,
|
||||
Path("bin" if sys.platform == "win32" else "lib") / dll_artifact_filename,
|
||||
Path("bin" if _is_windows_env(target_triple) else "lib")
|
||||
/ dll_artifact_filename,
|
||||
)
|
||||
# copy artifact for linking only on windows
|
||||
if sys.platform == "win32":
|
||||
lib_artifact_filename = _generate_lib_artifact_name(target_name)
|
||||
if _is_windows_env(target_triple):
|
||||
lib_artifact_filename = _generate_lib_artifact_name(target_name, target_triple)
|
||||
yield FileCopyInfo(
|
||||
target_directory / lib_artifact_filename,
|
||||
Path("lib") / lib_artifact_filename,
|
||||
@@ -187,6 +220,11 @@ def resolve_cmake_copy(ctx: ArtifactContext) -> Iterator[TextCopyInfo]:
|
||||
metadata = ctx.metadata
|
||||
target_name = extractor.get_target_name()
|
||||
|
||||
# get triple info
|
||||
target_triple = extractor.get_host_triple()
|
||||
if ctx.options.target is not None:
|
||||
target_triple = ctx.options.target
|
||||
|
||||
# compute fallback name
|
||||
cmake_fallback_name = _sanitize_name(target_name)
|
||||
# get namespace and target name with fallback
|
||||
@@ -202,8 +240,8 @@ def resolve_cmake_copy(ctx: ArtifactContext) -> Iterator[TextCopyInfo]:
|
||||
properties = CMakeProperties(
|
||||
cmake_namespace_name,
|
||||
cmake_target_name,
|
||||
_generate_dll_artifact_name(target_name),
|
||||
_generate_lib_artifact_name(target_name),
|
||||
_generate_dll_artifact_name(target_name, target_triple),
|
||||
_generate_lib_artifact_name(target_name, target_triple),
|
||||
extractor.get_version(),
|
||||
)
|
||||
render = CMakeRender(properties)
|
||||
|
||||
@@ -9,6 +9,7 @@ themselves, so the accepted options and their semantics live in one place.
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from .utils import Triple
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -31,6 +32,10 @@ class Cli:
|
||||
"""Destination of the zip archive bundling the distribution tree, or
|
||||
``None`` when no archive is requested."""
|
||||
|
||||
target: Triple | None
|
||||
"""Target triple to pack for (e.g. ``x86_64-pc-windows-msvc``), or ``None``
|
||||
to pack for the host toolchain."""
|
||||
|
||||
|
||||
def parse() -> Cli:
|
||||
"""Parse command-line arguments into a :class:`Cli` instance.
|
||||
@@ -77,9 +82,20 @@ def parse() -> Cli:
|
||||
help="Path of the zip archive created from the dist-dir contents",
|
||||
metavar="ZIP",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--target",
|
||||
dest="target",
|
||||
action="store",
|
||||
type=Triple.parse,
|
||||
required=False,
|
||||
help="Target triple to pack for (e.g. x86_64-pc-windows-msvc). Defaults to the host toolchain.",
|
||||
metavar="TRIPLE",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return Cli(
|
||||
manifest=args.manifest,
|
||||
dist_dir=args.dist_dir,
|
||||
dist_zip=args.dist_zip,
|
||||
target=args.target,
|
||||
)
|
||||
|
||||
@@ -14,10 +14,11 @@ import subprocess
|
||||
from dataclasses import dataclass
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from re import Pattern, compile
|
||||
from typing import Any, Callable
|
||||
from semver import Version
|
||||
from . import utils
|
||||
from .utils import dict_chain_get, dict_typed_get, dict_typed_get_required, VERSION
|
||||
from .utils import Triple, VERSION, dict_chain_get, dict_typed_get, dict_typed_get_required
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -192,6 +193,12 @@ def wrap_metadata_errors[**P, R](func: Callable[P, R]) -> Callable[P, R]:
|
||||
return wrapper
|
||||
|
||||
|
||||
_SUBPROCESS_TIMEOUT_SEC: int = 10
|
||||
"""Timeout in seconds for the ``cargo``/``rustc`` subprocesses launched below."""
|
||||
|
||||
_HOST_TRIPLE_PATTERN: Pattern = compile(r"(?m)^host:\s*(\S+)$")
|
||||
"""The pattern for match host triple in ``rustc -vV``. The group 1 is the triple result."""
|
||||
|
||||
class MetadataExtractor:
|
||||
"""Access layer over ``cargo metadata`` and the OMRF metadata table.
|
||||
|
||||
@@ -208,6 +215,8 @@ class MetadataExtractor:
|
||||
"""The package item in cargo metadata's packages list pointing to user request package"""
|
||||
__metadata_target: dict[str, Any]
|
||||
"""The target item in package item's targets list pointing to the main target"""
|
||||
__host_triple: Triple
|
||||
"""The host toolchain triple resolved from ``rustc -vV``"""
|
||||
|
||||
def __init__(self, cargo_toml_path: Path) -> None:
|
||||
"""Run ``cargo metadata`` and locate the package and main target.
|
||||
@@ -221,6 +230,35 @@ class MetadataExtractor:
|
||||
self.__metadata_target = MetadataExtractor.__extract_metadata_target(
|
||||
self.__metadata_package, cargo_toml_path
|
||||
)
|
||||
self.__host_triple = MetadataExtractor.__extract_host_triple()
|
||||
|
||||
@staticmethod
|
||||
@wrap_metadata_errors
|
||||
def __extract_host_triple() -> Triple:
|
||||
"""Query ``rustc -vV`` and parse its ``host:`` line into a :class:`Triple`.
|
||||
|
||||
:raises RuntimeError: if rustc times out, exits with a non-zero status,
|
||||
or its output carries no ``host:`` line.
|
||||
"""
|
||||
rustc = os.getenv("OMRF_PACKER_RUSTC", "rustc")
|
||||
proc = subprocess.Popen(
|
||||
[rustc, "-vV"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=_SUBPROCESS_TIMEOUT_SEC)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.communicate()
|
||||
raise RuntimeError("fail to fetch host triple: timed out")
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(
|
||||
"fail to fetch host triple: "
|
||||
+ stderr.decode("utf-8", errors="ignore")
|
||||
)
|
||||
m = _HOST_TRIPLE_PATTERN.search(stdout.decode("utf-8", errors="strict"))
|
||||
if m is None:
|
||||
raise RuntimeError("can not find host triple in rustc -vV output")
|
||||
return Triple.parse(m.group(1))
|
||||
|
||||
@staticmethod
|
||||
@wrap_metadata_errors
|
||||
@@ -243,7 +281,7 @@ class MetadataExtractor:
|
||||
]
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=10)
|
||||
stdout, stderr = proc.communicate(timeout=_SUBPROCESS_TIMEOUT_SEC)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.communicate()
|
||||
@@ -349,3 +387,7 @@ class MetadataExtractor:
|
||||
def get_target_name(self) -> str:
|
||||
"""Return the name of the main library target."""
|
||||
return dict_typed_get_required(self.__metadata_target, "name", str)
|
||||
|
||||
def get_host_triple(self) -> Triple:
|
||||
"""Return the host toolchain triple resolved at construction time."""
|
||||
return self.__host_triple
|
||||
|
||||
@@ -4,6 +4,7 @@ Provides version parsing/checking, typed dictionary access helpers, name
|
||||
validation and template-directory resolution.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from re import Pattern, compile
|
||||
from typing import Any, overload
|
||||
@@ -129,3 +130,45 @@ def is_good_sentence(s: str) -> bool:
|
||||
:returns: True if given string don't contain any EOL chars, otherwise false.
|
||||
"""
|
||||
return _EOL_PATTERN.search(s) is None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Triple:
|
||||
"""A Rust target triple (e.g. ``x86_64-pc-windows-msvc``).
|
||||
|
||||
Decomposes a triple into its ``arch``/``vendor``/``operating_system``/
|
||||
``env`` components. Use :meth:`parse` to build one from a string and
|
||||
``str(triple)`` to reconstruct the canonical dashed form.
|
||||
"""
|
||||
|
||||
architecture: str
|
||||
"""Architecture component (e.g. ``x86_64``, ``aarch64``)."""
|
||||
vendor: str
|
||||
"""Vendor component (e.g. ``pc``, ``unknown``, ``apple``)."""
|
||||
operating_system: str
|
||||
"""Operating-system component (e.g. ``windows``, ``linux``, ``darwin``)."""
|
||||
environment: str | None
|
||||
"""Environment/toolchain component (e.g. ``gnu``, ``msvc``), or ``None`` for
|
||||
3-component triples."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
base = f"{self.architecture}-{self.vendor}-{self.operating_system}"
|
||||
return f"{base}-{self.environment}" if self.environment is not None else base
|
||||
|
||||
@staticmethod
|
||||
def parse(s: str) -> "Triple":
|
||||
"""Parse a triple string into a :class:`Triple`.
|
||||
|
||||
:raises ValueError: if the string has fewer than 3 or more than 4
|
||||
dash-separated components.
|
||||
"""
|
||||
parts = s.split("-")
|
||||
if len(parts) < 3 or len(parts) > 4:
|
||||
raise ValueError(f"invalid target triple: {s!r}")
|
||||
env = parts[3] if len(parts) == 4 else None
|
||||
return Triple(
|
||||
architecture=parts[0],
|
||||
vendor=parts[1],
|
||||
operating_system=parts[2],
|
||||
environment=env,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user