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 self.__opts = opts
# build essential instances # build essential instances
self.__extractor = MetadataExtractor(self.__opts.manifest) self.__extractor = MetadataExtractor(self.__opts.manifest)
self.__ctx = ArtifactContext(self.__extractor) self.__ctx = ArtifactContext(self.__opts, self.__extractor)
def run(self) -> None: def run(self) -> None:
"""Produce the distribution. """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. archiver then materializes.
""" """
import sys
import logging import logging
from typing import Iterator from typing import Iterator
from pathlib import Path from pathlib import Path
from dataclasses import dataclass from dataclasses import dataclass
from re import Pattern, compile from re import Pattern, compile
from .cli import Cli
from .utils import Triple
from .metadata import MetadataExtractor, Metadata from .metadata import MetadataExtractor, Metadata
from .renders.cmake import CMakeProperties, CMakeRender from .renders.cmake import CMakeProperties, CMakeRender
from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender
@@ -19,17 +20,24 @@ from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender
class ArtifactContext: class ArtifactContext:
"""A wrapper of metadata with proper fallback""" """A wrapper of metadata with proper fallback"""
__opts: Cli
__extractor: MetadataExtractor __extractor: MetadataExtractor
__metadata: Metadata __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`. """Wrap an extractor and eagerly fetch its :class:`Metadata`.
:param extractor: The extractor to wrap and read from. :param extractor: The extractor to wrap and read from.
""" """
self.__opts = opts
self.__extractor = extractor self.__extractor = extractor
self.__metadata = self.__extractor.get_metadata() self.__metadata = self.__extractor.get_metadata()
@property
def options(self) -> Cli:
"""The wrapped :class:`Cli` holding all command line arguments"""
return self.__opts
@property @property
def extractor(self) -> MetadataExtractor: def extractor(self) -> MetadataExtractor:
"""The wrapped :class:`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``.""" """Return the platform-appropriate dynamic-loadable file name for ``name``."""
match sys.platform: match triple.operating_system:
case "win32" | "cygwin": case x if x in _WINDOWS_ENV_IDENTS:
return f"{name}.dll" return f"{name}.dll"
case "linux" | "android" | "freebsd": case x if x in _LINUX_ENV_IDENTS:
return f"lib{name}.so" return f"lib{name}.so"
case "darwin" | "ios": case x if x in _MACOS_ENV_IDENTS:
return f"lib{name}.dylib" return f"lib{name}.dylib"
case _: case _:
raise RuntimeError("not supported system") 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``. """Return the Windows import-library file name for ``name``.
Only meaningful on Windows; returned regardless of platform for use by the 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_directory = extractor.get_target_directory() / "release"
target_name = extractor.get_target_name() 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 # 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( yield FileCopyInfo(
target_directory / dll_artifact_filename, 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 # copy artifact for linking only on windows
if sys.platform == "win32": if _is_windows_env(target_triple):
lib_artifact_filename = _generate_lib_artifact_name(target_name) lib_artifact_filename = _generate_lib_artifact_name(target_name, target_triple)
yield FileCopyInfo( yield FileCopyInfo(
target_directory / lib_artifact_filename, target_directory / lib_artifact_filename,
Path("lib") / lib_artifact_filename, Path("lib") / lib_artifact_filename,
@@ -187,6 +220,11 @@ def resolve_cmake_copy(ctx: ArtifactContext) -> Iterator[TextCopyInfo]:
metadata = ctx.metadata metadata = ctx.metadata
target_name = extractor.get_target_name() 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 # compute fallback name
cmake_fallback_name = _sanitize_name(target_name) cmake_fallback_name = _sanitize_name(target_name)
# get namespace and target name with fallback # get namespace and target name with fallback
@@ -202,8 +240,8 @@ def resolve_cmake_copy(ctx: ArtifactContext) -> Iterator[TextCopyInfo]:
properties = CMakeProperties( properties = CMakeProperties(
cmake_namespace_name, cmake_namespace_name,
cmake_target_name, cmake_target_name,
_generate_dll_artifact_name(target_name), _generate_dll_artifact_name(target_name, target_triple),
_generate_lib_artifact_name(target_name), _generate_lib_artifact_name(target_name, target_triple),
extractor.get_version(), extractor.get_version(),
) )
render = CMakeRender(properties) render = CMakeRender(properties)
+16
View File
@@ -9,6 +9,7 @@ themselves, so the accepted options and their semantics live in one place.
import argparse import argparse
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from .utils import Triple
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -31,6 +32,10 @@ class Cli:
"""Destination of the zip archive bundling the distribution tree, or """Destination of the zip archive bundling the distribution tree, or
``None`` when no archive is requested.""" ``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: def parse() -> Cli:
"""Parse command-line arguments into a :class:`Cli` instance. """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", help="Path of the zip archive created from the dist-dir contents",
metavar="ZIP", 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() args = parser.parse_args()
return Cli( return Cli(
manifest=args.manifest, manifest=args.manifest,
dist_dir=args.dist_dir, dist_dir=args.dist_dir,
dist_zip=args.dist_zip, dist_zip=args.dist_zip,
target=args.target,
) )
+44 -2
View File
@@ -14,10 +14,11 @@ import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from functools import wraps from functools import wraps
from pathlib import Path from pathlib import Path
from re import Pattern, compile
from typing import Any, Callable from typing import Any, Callable
from semver import Version from semver import Version
from . import utils 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) @dataclass(frozen=True)
@@ -192,6 +193,12 @@ def wrap_metadata_errors[**P, R](func: Callable[P, R]) -> Callable[P, R]:
return wrapper 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: class MetadataExtractor:
"""Access layer over ``cargo metadata`` and the OMRF metadata table. """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""" """The package item in cargo metadata's packages list pointing to user request package"""
__metadata_target: dict[str, Any] __metadata_target: dict[str, Any]
"""The target item in package item's targets list pointing to the main target""" """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: def __init__(self, cargo_toml_path: Path) -> None:
"""Run ``cargo metadata`` and locate the package and main target. """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_target = MetadataExtractor.__extract_metadata_target(
self.__metadata_package, cargo_toml_path 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 @staticmethod
@wrap_metadata_errors @wrap_metadata_errors
@@ -243,7 +281,7 @@ class MetadataExtractor:
] ]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try: try:
stdout, stderr = proc.communicate(timeout=10) stdout, stderr = proc.communicate(timeout=_SUBPROCESS_TIMEOUT_SEC)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
proc.kill() proc.kill()
proc.communicate() proc.communicate()
@@ -349,3 +387,7 @@ class MetadataExtractor:
def get_target_name(self) -> str: def get_target_name(self) -> str:
"""Return the name of the main library target.""" """Return the name of the main library target."""
return dict_typed_get_required(self.__metadata_target, "name", str) 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. validation and template-directory resolution.
""" """
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from re import Pattern, compile from re import Pattern, compile
from typing import Any, overload 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. :returns: True if given string don't contain any EOL chars, otherwise false.
""" """
return _EOL_PATTERN.search(s) is None 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,
)