diff --git a/.github/workflows/dist.yml b/.github/workflows/dist.yml index fcd2bfc41..b54f2f522 100644 --- a/.github/workflows/dist.yml +++ b/.github/workflows/dist.yml @@ -212,6 +212,10 @@ jobs: SCCACHE_WEBDAV_USERNAME: ${{ secrets.WPI_ARTIFACTORY_USERNAME }} SCCACHE_WEBDAV_PASSWORD: ${{ secrets.WPI_ARTIFACTORY_TOKEN }} + - name: Ensure all headers are accounted for + run: | + python -m devtools ci scan-headers + - uses: actions/upload-artifact@v4 with: name: "pypi-meson-${{ runner.os }}-${{ runner.arch }}-${{ matrix.python_version }}" diff --git a/devtools/__main__.py b/devtools/__main__.py index c67f7d416..bbf2fc1d6 100644 --- a/devtools/__main__.py +++ b/devtools/__main__.py @@ -61,6 +61,26 @@ def develop(ctx: Context, package: str): project.develop() +@main.command() +@click.pass_obj +def scan_headers(ctx: Context): + """Run scan-headers on all projects""" + ok = True + for project in ctx.subprojects.values(): + if project.is_semiwrap_project(): + with ctx.handle_exception(f"scan-headers {project.name}"): + if not project.scan_headers(): + print( + "- ERROR:", + project.pyproject_path, + "does not wrap/ignore every header!", + ) + ok = False + + if not ok: + sys.exit(1) + + @main.command @click.argument("package", required=False) @click.pass_obj diff --git a/devtools/ci.py b/devtools/ci.py index c26563439..e1d824dc5 100644 --- a/devtools/ci.py +++ b/devtools/ci.py @@ -128,3 +128,27 @@ def build_meson_wheels(ctx: Context, no_test: bool, cross: T.Optional[str]): ) if not no_test: project.test(install_requirements=True) + + +@ci.command() +@click.pass_obj +def scan_headers(ctx: Context): + """Run scan-headers on all projects""" + ok = True + for project in ctx.subprojects.values(): + if project.is_semiwrap_project(): + if not project.cfg.ci_scan_headers: + print("- Skipping", project.name, file=sys.stderr) + continue + + with ctx.handle_exception(f"scan-headers {project.name}"): + if not project.scan_headers(): + print( + "- ERROR:", + project.pyproject_path, + "does not wrap/ignore every header!", + ) + ok = False + + if not ok: + sys.exit(1) diff --git a/devtools/config.py b/devtools/config.py index bf8c9373d..36d1f4d2e 100644 --- a/devtools/config.py +++ b/devtools/config.py @@ -13,6 +13,9 @@ class SubprojectConfig: #: Whether this should be built on roborio or not roborio: bool + #: Whether `ci scan-headers` should include this project + ci_scan_headers: bool = True + @dataclasses.dataclass class Parameters: diff --git a/devtools/subproject.py b/devtools/subproject.py index 6aca0346a..c42a76e23 100644 --- a/devtools/subproject.py +++ b/devtools/subproject.py @@ -48,6 +48,19 @@ def develop(self): def uninstall(self): run_pip("uninstall", "-y", self.pyproject_name) + def scan_headers(self): + """Returns True if no headers found or False if missing headers were found""" + result = run_cmd( + sys.executable, + "-m", + "semiwrap", + "scan-headers", + "--check", + cwd=self.path, + check=False, + ) + return result.returncode == 0 + def update_init(self): run_cmd( sys.executable, diff --git a/devtools/util.py b/devtools/util.py index c08ba555c..0253eaa81 100644 --- a/devtools/util.py +++ b/devtools/util.py @@ -44,9 +44,9 @@ def parse_input(value: typing.Any, spec: typing.Type[T], fname) -> T: raise _convert_validation_error(fname, ve) from None -def run_cmd(*args: str, cwd=None): +def run_cmd(*args: str, cwd=None, check=True): print("+", shlex.join(args)) - subprocess.check_call(args, cwd=cwd) + return subprocess.run(args, cwd=cwd, check=check) def run_pip(*args: str, cwd=None): diff --git a/rdev.toml b/rdev.toml index fb797f7eb..d30179108 100644 --- a/rdev.toml +++ b/rdev.toml @@ -98,6 +98,10 @@ roborio = true py_version = "wrapper" roborio = true +# practicality over purity - this is because we use a static +# library that only exists at build time +ci_scan_headers = false + [subprojects."robotpy-apriltag"] py_version = "wrapper" roborio = true diff --git a/subprojects/robotpy-cscore/pyproject.toml b/subprojects/robotpy-cscore/pyproject.toml index c2843be59..cdff88517 100644 --- a/subprojects/robotpy-cscore/pyproject.toml +++ b/subprojects/robotpy-cscore/pyproject.toml @@ -78,6 +78,22 @@ update_init = [ "cscore" ] +scan_headers_ignore = [ + # Only wrapping the C++ API + "cscore.h", + "cscore_c.h", + "cscore_raw.h", + + # Not needed + "cameraserver/CameraServerShared.h", + "vision/VisionPipeline.h", + "vision/VisionRunner.h", + + # Not wrapping OpenCV or cvnp + "cvnp/*", + "opencv2/*" +] + [tool.semiwrap.extension_modules."cscore._cscore"] name = "cscore" diff --git a/subprojects/robotpy-wpilib/pyproject.toml b/subprojects/robotpy-wpilib/pyproject.toml index 6b6621dfa..f5526c3c6 100644 --- a/subprojects/robotpy-wpilib/pyproject.toml +++ b/subprojects/robotpy-wpilib/pyproject.toml @@ -183,6 +183,7 @@ LiveWindow = "frc/livewindow/LiveWindow.h" # frc/motorcontrol DMC60 = "frc/motorcontrol/DMC60.h" Jaguar = "frc/motorcontrol/Jaguar.h" +Koors40 = "frc/motorcontrol/Koors40.h" MotorControllerGroup = "rpy/MotorControllerGroup.h" NidecBrushless = "frc/motorcontrol/NidecBrushless.h" PWMMotorController = "frc/motorcontrol/PWMMotorController.h" diff --git a/subprojects/robotpy-wpilib/semiwrap/Koors40.yml b/subprojects/robotpy-wpilib/semiwrap/Koors40.yml new file mode 100644 index 000000000..2b9af2bf6 --- /dev/null +++ b/subprojects/robotpy-wpilib/semiwrap/Koors40.yml @@ -0,0 +1,6 @@ +--- + +classes: + frc::Koors40: + methods: + Koors40: