From dbec1b4fca12a6794d2d4295540d79a4c972aa1b Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Wed, 5 Aug 2026 20:37:41 +0800 Subject: [PATCH] feat: use rust triple instead of python sys module after this change, cross compilation are supported in theory. --- packer/src/sarasacw_omrf_packer/__init__.py | 2 +- packer/src/sarasacw_omrf_packer/artifact.py | 66 ++++++++++++++++----- packer/src/sarasacw_omrf_packer/cli.py | 16 +++++ packer/src/sarasacw_omrf_packer/metadata.py | 46 +++++++++++++- packer/src/sarasacw_omrf_packer/utils.py | 43 ++++++++++++++ 5 files changed, 156 insertions(+), 17 deletions(-) diff --git a/packer/src/sarasacw_omrf_packer/__init__.py b/packer/src/sarasacw_omrf_packer/__init__.py index 1c2a8a7..4c1151c 100644 --- a/packer/src/sarasacw_omrf_packer/__init__.py +++ b/packer/src/sarasacw_omrf_packer/__init__.py @@ -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. diff --git a/packer/src/sarasacw_omrf_packer/artifact.py b/packer/src/sarasacw_omrf_packer/artifact.py index 544fa30..6b76187 100644 --- a/packer/src/sarasacw_omrf_packer/artifact.py +++ b/packer/src/sarasacw_omrf_packer/artifact.py @@ -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) diff --git a/packer/src/sarasacw_omrf_packer/cli.py b/packer/src/sarasacw_omrf_packer/cli.py index 245818b..3a54e1d 100644 --- a/packer/src/sarasacw_omrf_packer/cli.py +++ b/packer/src/sarasacw_omrf_packer/cli.py @@ -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, ) diff --git a/packer/src/sarasacw_omrf_packer/metadata.py b/packer/src/sarasacw_omrf_packer/metadata.py index 74e3ae8..bbea75f 100644 --- a/packer/src/sarasacw_omrf_packer/metadata.py +++ b/packer/src/sarasacw_omrf_packer/metadata.py @@ -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 diff --git a/packer/src/sarasacw_omrf_packer/utils.py b/packer/src/sarasacw_omrf_packer/utils.py index 7995b7b..f73a115 100644 --- a/packer/src/sarasacw_omrf_packer/utils.py +++ b/packer/src/sarasacw_omrf_packer/utils.py @@ -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, + )