mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-22 01:11:42 +00:00
[bazel][robotpy] Add mirror for robotpy's wpiuil and wpinet libraries (#8062)
Project import generated by Copybara. GitOrigin-RevId: 92ea93d1b47a82667044bd0af05f7fdb34d2c2c2
This commit is contained in:
12
shared/bazel/rules/robotpy/hatchlib_native_port/BUILD.bazel
Normal file
12
shared/bazel/rules/robotpy/hatchlib_native_port/BUILD.bazel
Normal file
@@ -0,0 +1,12 @@
|
||||
load("@allwpilib_pip_deps//:requirements.bzl", "requirement")
|
||||
load("@rules_python//python:defs.bzl", "py_binary")
|
||||
|
||||
py_binary(
|
||||
name = "generate_native_lib_files",
|
||||
srcs = glob(["*.py"]),
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//shared/bazel/rules/robotpy:hack_pkgcfgs",
|
||||
requirement("semiwrap"),
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
This is a port of [hatch_nativelib](https://github.com/robotpy/hatch-nativelib/tree/main/src/hatch_nativelib). That tool is not librar-icized and required a fork.
|
||||
106
shared/bazel/rules/robotpy/hatchlib_native_port/config.py
Normal file
106
shared/bazel/rules/robotpy/hatchlib_native_port/config.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import dataclasses
|
||||
import pathlib
|
||||
import typing as T
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PcFileConfig:
|
||||
"""
|
||||
Contents of [[tool.hatch.build.hooks.nativelib.pcfile]] items
|
||||
"""
|
||||
|
||||
pcfile: str
|
||||
"""
|
||||
File to write pkgconf file to (relative to pyproject.toml)
|
||||
"""
|
||||
|
||||
description: T.Optional[str] = None
|
||||
"""Description of this package. If not specified, uses the first line of the package description."""
|
||||
|
||||
name: T.Optional[str] = None
|
||||
"""Name of this package. If not specified, is basename of pcfile without extension"""
|
||||
|
||||
version: T.Optional[str] = None
|
||||
"""If not specified, set to package version"""
|
||||
|
||||
includedir: T.Optional[str] = None
|
||||
"""Where include files can be found (relative to pyproject.toml)"""
|
||||
|
||||
libdir: T.Optional[str] = None
|
||||
"""Where the library is located. If not specified, it is next to pcfile"""
|
||||
|
||||
shared_libraries: T.Optional[T.List[str]] = None
|
||||
"""Name of shared libraries located in libdir (without extension)"""
|
||||
|
||||
libs_private: T.Optional[str] = None
|
||||
"""The link flags for private libraries not exposed to applications"""
|
||||
|
||||
requires: T.Optional[T.List[str]] = None
|
||||
"""
|
||||
Names of other packages this package requires. They must be installed
|
||||
at build time.
|
||||
"""
|
||||
|
||||
requires_private: T.Optional[T.List[str]] = None
|
||||
"""
|
||||
Names of private packages this package requires. They must be installed
|
||||
at build time.
|
||||
"""
|
||||
|
||||
extra_cflags: T.Optional[str] = None
|
||||
"""A list of extra compiler flags to be added to Cflags after header search path"""
|
||||
|
||||
extra_link_flags: T.Optional[str] = None
|
||||
"""A list of extra link flags to be added to Libs"""
|
||||
|
||||
variables: T.Optional[T.Dict[str, str]] = None
|
||||
"""
|
||||
Custom variables to add to the generated file. Prefix, libdir, includedir must not be specified."""
|
||||
|
||||
init_module: str = "auto"
|
||||
"""
|
||||
If specified, the name of the python module that will be written next to
|
||||
the .pc file which will load the shared_libraries
|
||||
"""
|
||||
|
||||
enable_if: T.Optional[str] = None
|
||||
"""
|
||||
This is a PEP 508 environment marker specification.
|
||||
|
||||
This pcfile will only be generated if the environment marker matches the current
|
||||
build environment
|
||||
"""
|
||||
|
||||
def get_name(self) -> str:
|
||||
if self.name:
|
||||
return self.name
|
||||
return self.get_pc_path().name[:-3]
|
||||
|
||||
def get_out_path(self) -> pathlib.Path:
|
||||
return self.get_pc_path().parent
|
||||
|
||||
def get_pc_path(self) -> pathlib.Path:
|
||||
pc_path = pathlib.PurePosixPath(self.pcfile)
|
||||
if pc_path.is_absolute():
|
||||
raise ValueError(f"pcfile must not be absolute (is {pc_path})")
|
||||
if not pc_path.name.endswith(".pc"):
|
||||
raise ValueError(f"pcfile must end with .pc (is {pc_path})")
|
||||
return pathlib.Path(pc_path)
|
||||
|
||||
def get_init_module(self) -> str:
|
||||
if self.init_module == "auto":
|
||||
name = self.get_pc_path().name[:-3]
|
||||
name = name.replace("-", "_").replace(".", "_")
|
||||
module = f"_init_{name}"
|
||||
else:
|
||||
module = self.init_module
|
||||
|
||||
if not module.isidentifier():
|
||||
raise ValueError(
|
||||
f"init_module must be a valid python identifier (got {module})"
|
||||
)
|
||||
return module
|
||||
|
||||
def get_init_module_path(self) -> pathlib.Path:
|
||||
module = self.get_init_module()
|
||||
return self.get_out_path() / f"{module}.py"
|
||||
@@ -0,0 +1,318 @@
|
||||
import functools
|
||||
import pathlib
|
||||
import platform
|
||||
import sys
|
||||
import typing as T
|
||||
|
||||
import pkgconf
|
||||
import tomli
|
||||
from packaging.markers import Marker
|
||||
|
||||
from shared.bazel.rules.robotpy.hack_pkgcfgs import hack_pkgconfig
|
||||
from shared.bazel.rules.robotpy.hatchlib_native_port.config import PcFileConfig
|
||||
from shared.bazel.rules.robotpy.hatchlib_native_port.validate import parse_input
|
||||
|
||||
# Port of https://github.com/robotpy/hatch-nativelib/blob/main/src/hatch_nativelib/plugin.py
|
||||
|
||||
INITPY_VARNAME = "pkgconf_pypi_initpy"
|
||||
|
||||
platform_sys = platform.system()
|
||||
is_windows = platform_sys == "Windows"
|
||||
is_macos = platform_sys == "Darwin"
|
||||
|
||||
|
||||
class NativelibHook:
|
||||
def __init__(self, output_pcfile, output_libinit, config, metadata):
|
||||
self.output_pcfile = output_pcfile
|
||||
self.output_libinit = output_libinit
|
||||
|
||||
self.config = config
|
||||
self.root_pth = output_pcfile.parent.parent.parent
|
||||
self.metadata = metadata
|
||||
|
||||
def initialize(self):
|
||||
for pcfg in self._pcfiles:
|
||||
self._generate_pcfile(pcfg, {})
|
||||
|
||||
def _get_pkg_from_path(self, path: pathlib.Path) -> str:
|
||||
rel = path.relative_to(self.root_pth)
|
||||
return str(rel).replace("/", ".").replace("\\", ".")
|
||||
|
||||
def _generate_pcfile(
|
||||
self, pcfg: PcFileConfig, build_data: T.Dict[str, T.Any]
|
||||
) -> pathlib.Path:
|
||||
|
||||
pcfile_rel = pcfg.get_pc_path()
|
||||
pcfile = self.output_pcfile
|
||||
prefix_rel = pcfile_rel.parent
|
||||
prefix_path = pcfile.parent
|
||||
|
||||
prefix = "${pcfiledir}"
|
||||
|
||||
# variables first
|
||||
variables = {}
|
||||
variables["prefix"] = prefix
|
||||
|
||||
if pcfg.includedir:
|
||||
increl = pathlib.PurePosixPath(pcfg.includedir).relative_to(
|
||||
prefix_rel.as_posix()
|
||||
)
|
||||
variables["includedir"] = f"${{prefix}}/{increl}"
|
||||
|
||||
if pcfg.shared_libraries:
|
||||
if pcfg.libdir:
|
||||
librel = pathlib.PurePosixPath(pcfg.libdir).relative_to(
|
||||
prefix_rel.as_posix()
|
||||
)
|
||||
variables["libdir"] = f"${{prefix}}/{librel}"
|
||||
else:
|
||||
variables["libdir"] = "${prefix}"
|
||||
|
||||
if pcfg.variables:
|
||||
for n in ("prefix", "includedir", "libdir", INITPY_VARNAME):
|
||||
if n in pcfg.variables:
|
||||
raise ValueError(f"variables may not contain {n}")
|
||||
|
||||
variables.update(variables)
|
||||
|
||||
# If there are libraries, generate _init_NAME.py for each
|
||||
if pcfg.shared_libraries:
|
||||
package = self._get_pkg_from_path(prefix_path)
|
||||
variables[INITPY_VARNAME] = f"{package}.{pcfg.get_init_module()}"
|
||||
self._generate_init_py(pcfg, prefix_path, build_data)
|
||||
|
||||
# .. not documented but it works?
|
||||
# eps = self.metadata.core.entry_points.setdefault("pkg_config", {})
|
||||
# eps[pcfg.get_name()] = package
|
||||
|
||||
contents = [f"{k}={v}" for k, v in variables.items()]
|
||||
contents.append("")
|
||||
|
||||
description = pcfg.description
|
||||
if description is None:
|
||||
description = self.metadata["description"]
|
||||
|
||||
if not description:
|
||||
raise ValueError(
|
||||
f"tool.hatch.build.hooks.nativelib.pcfile: description not provided for {pcfg.get_name()}"
|
||||
)
|
||||
|
||||
contents += [
|
||||
f"Name: {pcfg.get_name()}",
|
||||
f"Description: {description}",
|
||||
]
|
||||
|
||||
version = pcfg.version or self.metadata["version"]
|
||||
if version:
|
||||
contents.append(f"Version: {version}")
|
||||
|
||||
libs = []
|
||||
if pcfg.shared_libraries:
|
||||
libs.append("-L${libdir}")
|
||||
libs.extend(f"-l{lib}" for lib in pcfg.shared_libraries)
|
||||
|
||||
cflags = []
|
||||
if pcfg.includedir:
|
||||
cflags.append("-I${includedir}")
|
||||
|
||||
if pcfg.extra_cflags:
|
||||
cflags.append(pcfg.extra_cflags)
|
||||
|
||||
if pcfg.requires:
|
||||
contents.append(f"Requires: {' '.join(pcfg.requires)}")
|
||||
|
||||
if pcfg.requires_private:
|
||||
contents.append(f"Requires.private: {' '.join(pcfg.requires_private)}")
|
||||
|
||||
if libs:
|
||||
contents.append(f"Libs: {' '.join(libs)}")
|
||||
|
||||
if pcfg.libs_private:
|
||||
contents.append(f"Libs.private: {pcfg.libs_private}")
|
||||
|
||||
if cflags:
|
||||
contents.append(f"Cflags: {' '.join(cflags)}")
|
||||
|
||||
content = ("\n".join(contents)) + "\n"
|
||||
pcfile.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(pcfile, "w") as fp:
|
||||
fp.write(content)
|
||||
|
||||
return pcfile
|
||||
|
||||
def _generate_init_py(
|
||||
self,
|
||||
pcfg: PcFileConfig,
|
||||
prefix_path: pathlib.Path,
|
||||
build_data: T.Dict[str, T.Any],
|
||||
):
|
||||
libinit_py_rel = pcfg.get_init_module_path()
|
||||
self.root_pth / libinit_py_rel
|
||||
|
||||
libdir = prefix_path
|
||||
if pcfg.libdir:
|
||||
libdir = self.root_pth / pathlib.PurePosixPath(pcfg.libdir)
|
||||
libdir = pathlib.Path(str(libdir).replace("src/", "").replace("src\\", ""))
|
||||
|
||||
lib_paths = []
|
||||
assert pcfg.shared_libraries is not None
|
||||
for lib in pcfg.shared_libraries:
|
||||
lib_path = libdir / self._make_shared_lib_fname(lib)
|
||||
lib_paths.append(lib_path)
|
||||
|
||||
if pcfg.requires:
|
||||
requires = pcfg.requires
|
||||
else:
|
||||
requires = []
|
||||
|
||||
_write_libinit_py(self.output_libinit, lib_paths, requires)
|
||||
|
||||
def _make_shared_lib_fname(self, lib: str):
|
||||
if is_windows:
|
||||
return f"{lib}.dll"
|
||||
elif is_macos:
|
||||
return f"lib{lib}.dylib"
|
||||
else:
|
||||
return f"lib{lib}.so"
|
||||
|
||||
@functools.cached_property
|
||||
def _pcfiles(self) -> T.List[PcFileConfig]:
|
||||
pcfiles = []
|
||||
for i, raw_pc in enumerate(self.config.get("pcfile", [])):
|
||||
pcfile = parse_input(
|
||||
raw_pc,
|
||||
PcFileConfig,
|
||||
"pyproject.toml",
|
||||
f"tool.hatch.build.hooks.nativelib.pcfile[{i}]",
|
||||
)
|
||||
|
||||
if pcfile.enable_if and not Marker(pcfile.enable_if).evaluate():
|
||||
print(
|
||||
f"{pcfile.pcfile} skipped because enable_if did not match current environment"
|
||||
)
|
||||
continue
|
||||
|
||||
pcfiles.append(pcfile)
|
||||
|
||||
return pcfiles
|
||||
|
||||
|
||||
# TODO: this belongs in a separate script/api that can be used from multiple tools
|
||||
def _write_libinit_py(
|
||||
init_py: pathlib.Path,
|
||||
libs: T.List[pathlib.Path],
|
||||
requires: T.List[str],
|
||||
):
|
||||
"""
|
||||
:param init_py: the _init module for the library(ies) that is written out
|
||||
:param libs: for each library that is being initialized, this is the
|
||||
path to that library
|
||||
|
||||
:param requires: other pkgconf packages that these libraries depend on.
|
||||
Their init_py will be looked up and imported first.
|
||||
"""
|
||||
|
||||
contents = [
|
||||
"# This file is automatically generated, DO NOT EDIT",
|
||||
"# fmt: off",
|
||||
"",
|
||||
]
|
||||
|
||||
for req in requires:
|
||||
r = pkgconf.run_pkgconf(
|
||||
req, f"--variable={INITPY_VARNAME}", capture_output=True
|
||||
)
|
||||
# TODO: should this be a fatal error
|
||||
if r.returncode == 0:
|
||||
module = r.stdout.decode("utf-8").strip() # type: ignore[arg-type, union-attr]
|
||||
contents.append(f"import {module}")
|
||||
else:
|
||||
raise Exception("Could not find ", req)
|
||||
|
||||
if contents[-1] != "":
|
||||
contents.append("")
|
||||
|
||||
if libs:
|
||||
contents += [
|
||||
"def __load_library():",
|
||||
" from os.path import abspath, join, dirname, exists",
|
||||
]
|
||||
|
||||
if is_macos:
|
||||
contents += [" from ctypes import CDLL, RTLD_GLOBAL"]
|
||||
else:
|
||||
contents += [" from ctypes import cdll", ""]
|
||||
|
||||
if len(libs) > 1:
|
||||
contents.append(" libs = []")
|
||||
|
||||
contents.append(" root = abspath(dirname(__file__))")
|
||||
|
||||
for lib in libs:
|
||||
rel = lib.relative_to(init_py.parent)
|
||||
components = ", ".join(map(repr, rel.parts))
|
||||
|
||||
contents += [
|
||||
"",
|
||||
f" lib_path = join(root, {components})",
|
||||
"",
|
||||
" try:",
|
||||
]
|
||||
|
||||
if is_macos:
|
||||
load = "CDLL(lib_path, mode=RTLD_GLOBAL)"
|
||||
else:
|
||||
load = "cdll.LoadLibrary(lib_path)"
|
||||
|
||||
if len(libs) > 1:
|
||||
contents.append(f" libs.append({load})")
|
||||
else:
|
||||
contents.append(f" return {load}")
|
||||
|
||||
contents += [
|
||||
" except FileNotFoundError:",
|
||||
f" if not exists(lib_path):",
|
||||
f' raise FileNotFoundError("{lib.name} was not found on your system. Is this package correctly installed?")',
|
||||
]
|
||||
|
||||
if is_windows:
|
||||
contents.append(
|
||||
f' raise Exception("{lib.name} could not be loaded. Do you have Visual Studio C++ Redistributible installed?")'
|
||||
)
|
||||
else:
|
||||
contents.append(
|
||||
f' raise FileNotFoundError("{lib.name} could not be loaded. There is a missing dependency.")'
|
||||
)
|
||||
|
||||
if len(libs) > 1:
|
||||
contents += [" return libs"]
|
||||
|
||||
contents += ["", "__lib = __load_library()", ""]
|
||||
|
||||
content = ("\n".join(contents)) + "\n"
|
||||
|
||||
init_py.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(init_py, "w") as fp:
|
||||
fp.write(content)
|
||||
|
||||
|
||||
def main():
|
||||
pyproject_toml = sys.argv[1]
|
||||
libinit_file = pathlib.Path(sys.argv[2])
|
||||
pc_file = pathlib.Path(sys.argv[3])
|
||||
pkgcfgs = [pathlib.Path(x) for x in sys.argv[4:]]
|
||||
|
||||
hack_pkgconfig(pkgcfgs)
|
||||
|
||||
with open(pyproject_toml, "rb") as fp:
|
||||
raw_config = tomli.load(fp)
|
||||
|
||||
nativelib_cfg = raw_config["tool"]["hatch"]["build"]["hooks"]["nativelib"]
|
||||
metadata = raw_config["project"]
|
||||
|
||||
generator = NativelibHook(pc_file, libinit_file, nativelib_cfg, metadata)
|
||||
generator.initialize()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
43
shared/bazel/rules/robotpy/hatchlib_native_port/validate.py
Normal file
43
shared/bazel/rules/robotpy/hatchlib_native_port/validate.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import typing
|
||||
|
||||
import validobj.validation
|
||||
from validobj import errors
|
||||
|
||||
T = typing.TypeVar("T")
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _convert_validation_error(
|
||||
fname, ve: errors.ValidationError, prefix: str
|
||||
) -> ValidationError:
|
||||
locs = []
|
||||
msgs = []
|
||||
|
||||
e: typing.Optional[BaseException] = ve
|
||||
while e is not None:
|
||||
|
||||
if isinstance(e, errors.WrongFieldError):
|
||||
locs.append(f".{e.wrong_field}")
|
||||
elif isinstance(e, errors.WrongListItemError):
|
||||
locs.append(f"[{e.wrong_index}]")
|
||||
else:
|
||||
msgs.append(str(e))
|
||||
|
||||
e = e.__cause__
|
||||
|
||||
loc = "".join(locs)
|
||||
if loc.startswith("."):
|
||||
loc = loc[1:]
|
||||
msg = "\n ".join(msgs)
|
||||
vmsg = f"{fname}: {prefix}{loc}:\n {msg}"
|
||||
return ValidationError(vmsg)
|
||||
|
||||
|
||||
def parse_input(value: typing.Any, spec: typing.Type[T], fname, prefix: str) -> T:
|
||||
try:
|
||||
return validobj.validation.parse_input(value, spec)
|
||||
except errors.ValidationError as ve:
|
||||
raise _convert_validation_error(fname, ve, prefix) from None
|
||||
Reference in New Issue
Block a user