feat: use rust triple instead of python sys module

after this change, cross compilation are supported in theory.
This commit is contained in:
2026-08-05 20:37:41 +08:00
parent 520bfc2054
commit dbec1b4fca
5 changed files with 156 additions and 17 deletions
+1 -1
View File
@@ -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.
+52 -14
View File
@@ -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)
+16
View File
@@ -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,
)
+44 -2
View File
@@ -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
+43
View File
@@ -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,
)