# The PyPI PROJECT name is "xarm-python-sdk", but the sdist is published under # the PEP 625 normalised name with underscores -- xarm_python_sdk-1.18.4.tar.gz. # The hyphenated filename 404s. %%pypi_source takes the name it is given # verbatim for both the directory and the filename, so this global has to be # the underscored form or Source0 is a dead URL: # %%pypi_source xarm_python_sdk -> .../source/x/xarm_python_sdk/xarm_python_sdk-1.18.4.tar.gz 200 # %%pypi_source xarm-python-sdk -> .../source/x/xarm-python-sdk/xarm-python-sdk-1.18.4.tar.gz 404 # (The directory component is normalised server-side and works either way; the # filename is not.) %global pypi_name xarm_python_sdk # The importable top-level module. %global module xarm # The binary RPM name, minus the pythonX- prefix. "xarm-python-sdk" would give # python3-xarm-python-sdk, which repeats "python" twice; Fedora's Python naming # guidelines say to drop the redundant component, so this is python3-xarm-sdk. # The canonical dist name is still provided automatically by # %%pyproject_save_files as python3dist(xarm-python-sdk), so anything that # depends on the PyPI name resolves without help. %global srcname xarm-sdk Name: python-%{srcname} Version: 1.18.4 Release: 1%{?dist} Summary: Python SDK for UFACTORY xArm, 850 and Lite 6 robot arms # VERIFIED FROM THE TARBALL, not from the GitHub API and not from PyPI. # # LICENSE in the sdist is the three-clause BSD text verbatim: the two # redistribution clauses plus "Neither the name of the copyright holder nor the # names of its contributors may be used to endorse or promote products derived # from this software without specific prior written permission." # Copyright (c) 2018, UFACTORY Inc. That is BSD-3-Clause. # # The "AND MIT" is not decoration. Four files that this RPM installs carry # "Software License Agreement (MIT License)" in their own headers rather than # the BSD one: # xarm/core/comm/base.py # xarm/core/wrapper/__init__.py # xarm/tools/list_ports.py # xarm/x3/parse.py # All four are UFACTORY-copyright, so this is upstream being inconsistent with # itself rather than third-party code, and MIT is more permissive than # BSD-3-Clause -- but the files ship, so the expression has to say so. The # other 102 headers say BSD. %%prep asserts the count, so if upstream tidies # this up (or adds a fifth), the build fails and the License field gets # re-examined instead of quietly going stale. # # ONE non-UFACTORY, NON-PERMISSIVE file exists in the sdist: # doc/tool/markdown_doc.py, "Written by Patrick Laban and Geremy Condra, # Licensed under GPLv3". It is an author's doc-generation helper, it is not part # of the xarm package, and it is deliberately NOT shipped -- %%files lists # doc/api and doc/UF_ModbusTCP_Manual.md individually rather than doc/, so # doc/tool/ stays out and GPL-3.0-only stays out of the expression. %%prep # asserts that file is still the only GPL one. # # Why this is worth spelling out: PyPI's metadata for xarm-python-sdk has NO # licence field at all. pyproject.toml has # # license = "BSD-3-Clause" # # license-files = ["LICEN[CS]E*"] # commented out, and setup.py has # license='BSD' commented out too -- most # likely because upstream still claims requires-python >= 3.5 and PEP 639 # metadata needs a recent setuptools/hatchling. So every automated licence # reading of this package returns None, and the only authority is the tarball. # The setup.py classifier "License :: OSI Approved :: BSD License" survives, # which is consistent but not specific enough to be the source of truth. License: BSD-3-Clause AND MIT URL: https://github.com/xArm-Developer/xArm-Python-SDK Source0: %{pypi_source %{pypi_name} %{version} tar.gz} BuildArch: noarch BuildRequires: python3-devel # pyserial is an OPTIONAL import in this SDK and is declared nowhere upstream: # requirements.txt is empty and there is no [project.dependencies] table, so # %%pyproject_buildrequires generates nothing for it. Two shipped modules import # it at module scope -- xarm/core/comm/serial_port.py ("import serial") and # xarm/tools/list_ports.py ("from serial.tools import list_ports") -- so # %%pyproject_check_import cannot import them without it. It is a BuildRequires # so %%check covers the serial transport, and only a Recommends at runtime, # because the Lite 6 is an Ethernet device (see %%description). BuildRequires: python3-pyserial %global _description %{expand: The official Python SDK from UFACTORY for their robot arms -- xArm 5/6/7, the 850, and the Lite 6. It speaks UFACTORY's UXBUS protocol to the arm's control box and exposes it as one class, xarm.wrapper.XArmAPI: Cartesian and joint motion, forward and inverse kinematics, velocity and servo-streaming modes, teach (free-drive) mode, gripper and GPIO control, trajectory record and playback, a Modbus/TCP passthrough, and the error and state machine. Poses are [x, y, z, roll, pitch, yaw] in millimetres and, by default, degrees (XArmAPI(is_radian=False) is the default); get_position_aa()/set_position_aa() offer the same pose as a rotation vector instead, which avoids the Euler convention question entirely. Transport is Ethernet: the SDK opens Modbus/TCP port 502 for control and 30000-30003 for the telemetry report streams. A legacy direct-to-arm serial transport also exists in the code and needs python3-pyserial, which is why that is a Recommends; it is not the path a Lite 6 or an xArm control box uses. No hardware is needed to install this package, but it is useless without an arm to talk to.} %description %_description %package -n python3-%{srcname} Summary: %{summary} # Ethernet is the supported transport; the serial path is legacy and its two # modules are the only thing in the SDK that needs a third-party import. # Weak, so `dnf install python3-xarm-sdk` pulls it by default but the package # is not broken in a minimal image that excludes recommends. Recommends: python3-pyserial %description -n python3-%{srcname} %_description %prep %autosetup -n %{pypi_name}-%{version} # --------------------------------------------------------------------------- # Licence assertions. These exist so that the License: field above cannot go # stale silently across a version bump. Each is written in the "I expect a # match, fail if absent" direction and negated with `if ! ...; then`, or in the # "I expect NO match" direction as `if ... ; then fail; fi`. Never # `grep -q X && fail`, which succeeds-by-not-matching and inverts the test. # --------------------------------------------------------------------------- # 1. LICENSE is still BSD-3-Clause and not BSD-2-Clause. The third clause is # the whole difference between the two, so test for the third clause, not # for the word "BSD". if ! grep -q 'Neither the name of the copyright holder' LICENSE; then echo "ERROR: LICENSE no longer contains the BSD-3-Clause 'Neither the" >&2 echo "name' clause. It may have become BSD-2-Clause or something else." >&2 echo "Re-read it and fix License: before shipping." >&2 exit 1 fi # 2. Exactly four INSTALLED files declare MIT in their own header. If upstream # normalises them to BSD, or adds a fifth, "BSD-3-Clause AND MIT" is wrong # in one direction or the other. mit_count=$(grep -rl 'Software License Agreement (MIT License)' %{module} | wc -l) if [ "$mit_count" -ne 4 ]; then echo "ERROR: expected 4 MIT-headed files under %{module}/, found $mit_count:" >&2 grep -rl 'Software License Agreement (MIT License)' %{module} >&2 echo "License: is 'BSD-3-Clause AND MIT' because of exactly those four." >&2 exit 1 fi # 3. The only GPL file in the sdist is still doc/tool/markdown_doc.py, which is # not shipped. If a GPL file appears under the xarm package or the doc paths # %%files does list, the expression is wrong and the package may not be # redistributable as BSD. gpl_shipped=$(grep -rli 'GPL' %{module} doc/api doc/UF_ModbusTCP_Manual.md example || true) if [ -n "$gpl_shipped" ]; then echo "ERROR: GPL text found in files this RPM ships:" >&2 echo "$gpl_shipped" >&2 exit 1 fi test -f doc/tool/markdown_doc.py || { echo "ERROR: doc/tool/markdown_doc.py is gone. It was the one GPLv3 file" >&2 echo "in the sdist and the reason %%files enumerates doc/ selectively." >&2 echo "If upstream dropped it, this guard and that %%files list can be" >&2 echo "simplified -- but decide that deliberately." >&2 exit 1 } # --------------------------------------------------------------------------- # Keep the PEP 517 backend shim out of the WHEEL. # # pyproject.toml declares # build-backend = "build_backend" # backend-path = ["xarm"] # so the backend is xarm/build_backend.py, a file INSIDE the package it builds, # whose whole body is `from hatchling.build import *` (or setuptools on Python # < 3.8). Because it lives under xarm/, hatchling's `packages = ["xarm"]` puts # it in the wheel, and pip installs it to site-packages as xarm.build_backend. # # That is worse than cosmetic here, and in a specific way this project has been # bitten by before: hatchling IS present in the build root (it is the build # backend), so `import xarm.build_backend` SUCCEEDS during %%check and FAILS on # a user's system, where hatchling is not installed. A %%check that only passes # because of a build-time-only dependency is a %%check that tests the build # root rather than the RPM. Excluding the file removes both the failure and the # false pass; %%check asserts it is absent from the import list. # # The exclude is scoped to the wheel target only, so the backend is still # importable from backend-path during the build itself. if grep -q '^exclude' pyproject.toml; then echo "ERROR: pyproject.toml already has an 'exclude' key; appending a" >&2 echo "second [tool.hatch.build.targets.wheel] exclude would be ambiguous." >&2 exit 1 fi if ! grep -q '^\[tool\.hatch\.build\.targets\.wheel\]' pyproject.toml; then echo "ERROR: [tool.hatch.build.targets.wheel] is gone from pyproject.toml;" >&2 echo "the exclude below would land in whatever table happens to be last." >&2 exit 1 fi python3 - <<'EOF' import re src = open('pyproject.toml', encoding='utf-8').read() marker = '[tool.hatch.build.targets.wheel]\n' assert src.count(marker) == 1, 'wheel target table is not unique' src = src.replace(marker, marker + 'exclude = ["xarm/build_backend.py"]\n', 1) open('pyproject.toml', 'w', encoding='utf-8').write(src) EOF grep -q '^exclude = \["xarm/build_backend.py"\]$' pyproject.toml || exit 1 # --------------------------------------------------------------------------- # Strip "#!/usr/bin/env python3" from the library modules. # # 42 of the shipped modules carry a shebang while being plain importable # library files -- this package installs no console_script and nothing under # /usr/bin -- which rpmlint reports as 56 non-executable-script errors (one per # module per interpreter path). Fedora's guidance is that a library module with # a shebang is either wrong about being a script or wrong about its mode; since # none of these is an entry point, the shebang is what is wrong. # # brp-mangle-shebangs rewrites shebangs, it does not remove them, so this has to # happen here. Deleting line 1 of a module that has no __main__ block changes # nothing about how it imports; %%check import-tests all 53 afterwards. # # The count is asserted in both directions: at least 20 files must be touched # (a silent zero would mean the sed pattern stopped matching and the rpmlint # errors came back), and no file may keep a shebang afterwards. # A note on the \r in the patterns below, because it cost a build: FIVE files in # this sdist have CRLF line endings while the rest have LF -- # xarm/version.py xarm/core/comm/socket_port.py xarm/core/wrapper/uxbus_cmd.py # xarm/wrapper/xarm_api.py xarm/x3/base.py # so their first line is "#!/usr/bin/env python3\r". An unanchored grep matches # it and a `$`-anchored sed does not, which is how the first attempt stripped 23 # of 27 shebangs and left the four biggest and most important modules (including # xarm_api.py, the public API) with theirs. The post-check caught it, which is # the only reason this comment is here and not a shipped bug. # # The line endings themselves are left alone: mixed CRLF in Python source is # ugly but harmless, and rewriting whole files to fix cosmetics is a worse trade # than documenting it. # Two shebang forms occur: 41 files use "#!/usr/bin/env python3" and one, # xarm/core/utils/__init__.py, uses "#!/usr/bin/python3". Matching only the # first form left that one file behind and one rpmlint error standing. shebang_files () { grep -rlE '^#!(/usr/bin/env python3|/usr/bin/python3)' %{module}; } line1_shebang () { head -1 "$1" | grep -qE '^#!(/usr/bin/env python3|/usr/bin/python3)'; } before=0 for f in $(shebang_files); do line1_shebang "$f" && before=$((before + 1)) done if [ "$before" -lt 20 ]; then echo "ERROR: only $before modules under %{module}/ have a shebang on line 1;" >&2 echo "expected >= 20 (1.18.4 has 42). The pattern has stopped matching." >&2 exit 1 fi for f in $(shebang_files); do sed -i -E '1{/^#!(\/usr\/bin\/env python3|\/usr\/bin\/python3)\r*$/d}' "$f" done # Check LINE 1 only. A plain recursive grep would also hit the blockly # code-generator templates, which legitimately contain that string in the source # they emit, and would fail this guard forever for the wrong reason. after=0 for f in $(shebang_files); do if line1_shebang "$f"; then echo "STILL SHEBANGED: $f" >&2 after=$((after + 1)) fi done if [ "$after" -ne 0 ]; then echo "ERROR: $after modules still carry a line-1 shebang after stripping." >&2 exit 1 fi echo "stripped the line-1 shebang from $before library modules" %generate_buildrequires %pyproject_buildrequires %build %pyproject_wheel %install %pyproject_install %pyproject_save_files -l %{module} # Verify against the installed tree rather than the build log. test -f %{buildroot}%{python3_sitelib}/%{module}/wrapper/xarm_api.py || exit 1 test -f %{buildroot}%{python3_sitelib}/%{module}/x3/xarm.py || exit 1 test -f %{buildroot}%{python3_sitelib}/%{module}/core/comm/socket_port.py || exit 1 # The exclude in %%prep must have taken effect. If this file is back, %%check's # import test starts depending on hatchling being installed (see the %%prep note). test ! -e %{buildroot}%{python3_sitelib}/%{module}/build_backend.py || { echo "ERROR: xarm/build_backend.py was installed after all." >&2 exit 1 } # The sdist ships no .git (unlike the NaviNIBS addons) and no prebuilt # binaries, but assert it rather than assume it holds at the next bump. test ! -e %{buildroot}%{python3_sitelib}/%{module}/.git || exit 1 if find %{buildroot}%{python3_sitelib}/%{module} \ \( -name '*.so' -o -name '*.pyd' -o -name '*.dll' -o -name '*.exe' \ -o -name '*.dylib' -o -name '*.a' \) -print | grep -q .; then echo "ERROR: prebuilt binaries under %{module}/. Fedora forbids shipping" >&2 echo "them and this SDK had none in 1.18.4 -- investigate before bumping." >&2 find %{buildroot}%{python3_sitelib}/%{module} \ \( -name '*.so' -o -name '*.pyd' -o -name '*.dll' -o -name '*.exe' \ -o -name '*.dylib' -o -name '*.a' \) -print >&2 exit 1 fi # The API-surface check %%check runs. Kept as a file so it can be read and # re-run by hand; see docs/xarm-lite6-api-mapping.md, which it enforces. install -Dpm0644 /dev/stdin %{_builddir}/api-surface-check.py <<'EOF' import inspect import os import sys import xarm from xarm.wrapper import XArmAPI # Prove we are inspecting the INSTALLED tree and not the unpacked sdist. The # source directory is %%check's cwd and it contains a xarm/ directory, so # without -P (and this assertion) this script could pass while testing the # source, which would say nothing about the RPM. expected_root = os.environ.get('XARM_EXPECTED_ROOT') if not expected_root: sys.exit('ERROR: XARM_EXPECTED_ROOT is unset; cannot tell whether the ' 'installed tree or the source tree is under test.') actual_root = os.path.dirname(os.path.dirname(os.path.abspath(xarm.__file__))) if os.path.realpath(actual_root) != os.path.realpath(expected_root): sys.exit('ERROR: imported xarm from %s, expected it under %s. This would ' 'have tested the source tree, not the built package.' % (actual_root, expected_root)) print('OK: inspecting the installed tree at %s' % (xarm.__file__,)) # Each of the 16 abc.ABC methods of biomaglab/tms-robot-control's # robot/robots/robot.py, mapped to the XArmAPI attribute names a Lite 6 driver # would call to implement it. The mapping, its units and its caveats are # documented in docs/xarm-lite6-api-mapping.md; this asserts the SDK side of it # still exists by name, so that a version bump which renames or drops an entry # point fails the build instead of silently invalidating the design document. # # read_force_sensor is present as a KEY WITH NO MAPPING on purpose. A bare # Lite 6 has no six-axis force/torque sensor, and get_ft_sensor_data() on one # returns rc=0 with an all-zero wrench -- a success code and a plausible # reading, which is the fail-open shape this repo keeps getting burnt by. # Asserting get_ft_sensor_data() exists would be true and would mean nothing; # it is checked below under a name that cannot be mistaken for a mapping. MAPPING = { 'connect': ['connect'], 'disconnect': ['disconnect'], 'is_connected': ['connected'], 'initialize': ['motion_enable', 'clean_error', 'set_mode', 'set_state'], 'get_pose': ['get_position', 'get_position_aa'], 'is_moving': ['get_is_moving', 'state'], 'is_error_state': ['has_error', 'get_err_warn_code', 'error_code'], 'read_force_sensor': [], 'move_linear': ['set_position'], 'move_circular': ['move_circle'], 'dynamic_motion': ['set_servo_cartesian', 'set_servo_cartesian_aa', 'vc_set_cartesian_velocity'], 'stop_robot': ['set_state', 'emergency_stop'], 'enable_free_drive': ['set_mode', 'set_state'], 'disable_free_drive': ['set_mode', 'set_state'], 'close': ['disconnect'], 'clean_errors': ['clean_error', 'clean_warn'], } # Force-adjacent entry points, named apart from MAPPING so that their presence # is never read as read_force_sensor being satisfiable. get_joints_torque is # the only force-ish signal a BARE Lite 6 actually produces, and it is # joint-space motor-current-derived torque, not a calibrated TCP wrench. FORCE_ADJACENT = ['get_ft_sensor_data', 'get_joints_torque', 'joints_torque', 'ft_ext_force', 'ft_raw_force'] if len(MAPPING) != 16: sys.exit('ERROR: MAPPING has %d entries; robot.py declares 16 abstract ' 'methods. The table has drifted.' % len(MAPPING)) missing = {} for abc_name, attrs in sorted(MAPPING.items()): gone = [a for a in attrs if getattr(XArmAPI, a, None) is None] if gone: missing[abc_name] = gone gone_force = [a for a in FORCE_ADJACENT if getattr(XArmAPI, a, None) is None] if gone_force: missing['(force-adjacent; NOT read_force_sensor)'] = gone_force if missing: print('ERROR: XArmAPI is missing entry points that the Lite 6 <-> ' 'tms-robot-control mapping depends on:', file=sys.stderr) for key, val in sorted(missing.items()): print(' %s: %s' % (key, ', '.join(val)), file=sys.stderr) print('Re-check docs/xarm-lite6-api-mapping.md against this SDK version.', file=sys.stderr) sys.exit(1) checked = sorted({a for v in MAPPING.values() for a in v} | set(FORCE_ADJACENT)) # A mapping table that has been emptied would pass every loop above while # inspecting nothing. 1.18.4 checks 26 attributes; refuse a large regression. if len(checked) < 24: sys.exit('ERROR: only %d attributes were checked (expected >= 24). The ' 'mapping table has been hollowed out.' % len(checked)) mapped = sorted(k for k, v in MAPPING.items() if v) print('OK: %d of the 16 tms-robot-control ABC methods have a named XArmAPI ' 'equivalent: %s' % (len(mapped), ', '.join(mapped))) print('OK: read_force_sensor is intentionally unmapped (no F/T sensor on a ' 'bare Lite 6; get_ft_sensor_data returns rc=0 and an all-zero wrench).') print('OK: %d XArmAPI attributes verified: %s' % (len(checked), ', '.join(checked))) # The whole mapping document is written in mm and degrees, which is only true # because is_radian defaults to False. Assert the default rather than trusting # the docstring: if upstream flips it, every angle in a driver written from # that document becomes wrong by 57.3x, silently. default = inspect.signature(XArmAPI.__init__).parameters['is_radian'].default if default is not False: sys.exit('ERROR: XArmAPI(is_radian=...) now defaults to %r, not False. ' 'docs/xarm-lite6-api-mapping.md assumes degrees.' % (default,)) # do_not_open=True must not touch the network. This is also the only way to # exercise the constructor without hardware. arm = XArmAPI(port=None, do_not_open=True) if arm.default_is_radian is not False: sys.exit('ERROR: default_is_radian is %r, expected False (degrees).' % (arm.default_is_radian,)) if arm.connected: sys.exit('ERROR: do_not_open=True produced a connected instance.') print('OK: default unit system is degrees; do_not_open=True honoured and no ' 'connection attempted.') EOF %check # NO HARDWARE. Nothing below talks to an arm, and nothing below should be read # as evidence that this SDK drives one correctly. What it does establish: # (a) every shipped module imports under Fedora's Python 3.14, which is what # upstream's requires-python = ">=3.5" does not establish -- that floor is # four interpreter series stale and the blockly helpers still carry # invalid escape sequences that emit SyntaxWarning on 3.12+; # (b) the XArmAPI attribute names that docs/xarm-lite6-api-mapping.md is # written against still exist, so the design document cannot be # invalidated by an upstream rename without this build failing; # (c) angles are degrees by default, which the whole mapping depends on. test -s %{_pyproject_modules} || { echo "ERROR: %{_pyproject_modules} is missing or empty, so there would be" >&2 echo "nothing to import-check and %%check would pass having tested" >&2 echo "nothing at all." >&2 exit 1 } # 1.18.4 has 49 importable modules. A collapse to a handful means the wheel # came out wrong (or %%pyproject_save_files did) and the import check would # then pass while covering almost nothing. n_modules=$(wc -l < %{_pyproject_modules}) if [ "$n_modules" -lt 40 ]; then echo "ERROR: only $n_modules modules in the import list; expected >= 40." >&2 cat %{_pyproject_modules} >&2 exit 1 fi # The modules that matter, named explicitly. xarm.wrapper.xarm_api is the # public API; xarm.core.comm.serial_port is the one that needs pyserial and so # is the only thing proving the BuildRequires above is doing any work. for m in xarm xarm.wrapper xarm.wrapper.xarm_api xarm.x3.xarm xarm.x3.base \ xarm.core.comm.socket_port xarm.core.comm.serial_port \ xarm.tools.list_ports; do grep -qxF "$m" %{_pyproject_modules} || { echo "ERROR: $m is not in the import list." >&2 cat %{_pyproject_modules} >&2 exit 1 } done # And the module that must NOT be there. See the %%prep note: if the PEP 517 # backend shim is installed, importing it needs hatchling, which is present # here and absent on a user's system -- so this check would go green on a # package that fails for users. if grep -qxF 'xarm.build_backend' %{_pyproject_modules}; then echo "ERROR: xarm.build_backend is in the import list. The %%prep exclude" >&2 echo "did not hold, and %%pyproject_check_import would pass only because" >&2 echo "hatchling happens to be installed in the build root." >&2 exit 1 fi # xarm.x3.grammar_coroutine CANNOT be imported on Fedora's Python and is # DEAD CODE there. Its body is # class CoroutineObject(object): # @asyncio.coroutine # and asyncio.coroutine was removed in Python 3.11, so importing it raises # AttributeError: module 'asyncio' has no attribute 'coroutine' # That is not a packaging defect and not something to patch: xarm/x3/base.py # picks between the two grammar modules by interpreter version -- # if sys.version_info.major >= 3 and sys.version_info.minor >= 5: # from .grammar_async import AsyncObject as BaseObject # else: # from .grammar_coroutine import CoroutineObject as BaseObject # -- so on anything >= 3.5 only grammar_async is ever reached, and that one # imports fine here. grammar_coroutine exists solely for the Python 3.5-3.10 # range that upstream's requires-python = ">=3.5" still claims. # # So it is filtered out, and the filter is bracketed by assertions in both # directions so that it cannot hollow the check out: # - the module must BE in the list before filtering (if upstream deletes the # file, this stops being a known-dead exclusion and we should know); # - base.py must still guard the import by version (if that gate goes away, # the module becomes reachable and filtering it would be hiding a real # runtime failure); # - the surviving count is re-checked below. grep -qxF 'xarm.x3.grammar_coroutine' %{_pyproject_modules} || { echo "ERROR: xarm.x3.grammar_coroutine is not in the import list, so the" >&2 echo "filter below has nothing to remove. Upstream may have deleted the" >&2 echo "module -- drop the filter -- or %%pyproject_save_files changed." >&2 exit 1 } if ! grep -q 'from \.grammar_coroutine import CoroutineObject' %{module}/x3/base.py; then echo "ERROR: xarm/x3/base.py no longer imports grammar_coroutine behind a" >&2 echo "version gate. If it is now imported unconditionally, it FAILS on" >&2 echo "Python 3.11+ (asyncio.coroutine is gone) and must not be filtered." >&2 exit 1 fi if ! grep -q 'sys\.version_info\.minor >= 5' %{module}/x3/base.py; then echo "ERROR: the interpreter-version gate around the grammar_coroutine" >&2 echo "import has changed shape. Re-read xarm/x3/base.py before trusting" >&2 echo "that the module is unreachable on Python 3.14." >&2 exit 1 fi grep -vxF 'xarm.x3.grammar_coroutine' %{_pyproject_modules} \ > %{_pyproject_modules}.filtered mv %{_pyproject_modules}.filtered %{_pyproject_modules} n_modules=$(wc -l < %{_pyproject_modules}) if [ "$n_modules" -lt 40 ]; then echo "ERROR: only $n_modules modules left after filtering; expected >= 40." >&2 cat %{_pyproject_modules} >&2 exit 1 fi echo "import-checking $n_modules modules:" cat %{_pyproject_modules} %pyproject_check_import # Importing anything from this SDK prints "SDK_VERSION: 1.18.4" to stdout -- # xarm/x3/base.py does it at module scope. Nothing below parses stdout, but # expect that line in the log and do not add an assertion that would trip on it. # # -P keeps the cwd (the unpacked sdist, which has a xarm/ directory of its own) # off sys.path, so the only importable xarm is the one in the buildroot; # XARM_EXPECTED_ROOT lets the script assert that rather than assume it. XARM_EXPECTED_ROOT=%{buildroot}%{python3_sitelib} \ PYTHONPATH=%{buildroot}%{python3_sitelib} \ %{python3} -sP %{_builddir}/api-surface-check.py %files -n python3-%{srcname} -f %{pyproject_files} %doc README.md %doc ReleaseNotes.md # doc/ is enumerated rather than globbed so that doc/tool/ -- which holds a # GPLv3 helper by a third party (see the %%prep guards) -- stays out of a # BSD-3-Clause AND MIT package. %doc doc/api %doc doc/UF_ModbusTCP_Manual.md # 70-odd runnable examples, including the Lite 6 gripper, the force-control and # the online-trajectory-planning ones. They are the practical reference for # writing a driver and they are all UFACTORY BSD-headed. They are documentation # only: the wheel installs the xarm package and nothing else, so nothing here # lands in site-packages. %doc example %changelog * Sun Sep 27 2026 Morgan Hough - 1.18.4-1 - Initial package for the mhough/neurofedora COPR. Class 1, new to Fedora: Fedora provides no python3dist(xarm-python-sdk). - License verified from LICENSE in the tarball, not from the GitHub API and not from PyPI, which has no licence field at all because pyproject.toml has its license/license-files keys commented out. LICENSE is BSD-3-Clause verbatim; four shipped files (xarm/core/comm/base.py, xarm/core/wrapper/__init__.py, xarm/tools/list_ports.py, xarm/x3/parse.py) carry MIT headers instead, hence "BSD-3-Clause AND MIT". %%prep asserts both, and asserts that the one GPLv3 file in the sdist (doc/tool/markdown_doc.py, third-party) is still the only one and is still not shipped. - Name is python-xarm-sdk / python3-xarm-sdk rather than python3-xarm-python-sdk: Fedora's Python naming guidelines drop the redundant "python". python3dist(xarm-python-sdk) is still provided automatically. - Source0 uses the underscored sdist name. The hyphenated filename PyPI's project page implies does not exist -- only xarm_python_sdk-1.18.4.tar.gz (230108 bytes) does. - Exclude xarm/build_backend.py from the wheel. It is the PEP 517 backend (build-backend = "build_backend", backend-path = ["xarm"]) and its body is `from hatchling.build import *`, so installing it would put a module in site-packages that imports fine in the build root -- where hatchling is present as the build backend -- and fails everywhere else. %%check asserts it is absent from the import list, so the false pass cannot come back quietly. - %%check import-tests all 49 modules on Python 3.14 (upstream still declares requires-python >= 3.5) and then asserts the XArmAPI attribute names that docs/xarm-lite6-api-mapping.md is written against, so an upstream rename fails the build instead of invalidating that document silently. read_force_sensor is deliberately left unmapped: a bare Lite 6 has no six-axis F/T sensor and get_ft_sensor_data() returns rc=0 with an all-zero wrench, so asserting it exists would be true and meaningless. - Filter xarm.x3.grammar_coroutine out of that import check, and only that one. It is dead code on any Python >= 3.5: xarm/x3/base.py selects grammar_async instead by interpreter version, and grammar_coroutine's body uses @asyncio.coroutine, which was removed in Python 3.11 -- so importing it raises AttributeError on f44. The filter is bracketed by assertions that the module is still in the list, that base.py still guards it by version, and that >= 40 modules survive, so it cannot hollow the check out or hide the day that module becomes reachable. - BuildRequires python3-pyserial (undeclared upstream; requirements.txt is empty) so the two serial-transport modules are import-tested; only a Recommends at runtime, because the Lite 6 is an Ethernet device. - Strip the line-1 shebang from the 42 library modules that carry one (41 use "#!/usr/bin/env python3", one uses "#!/usr/bin/python3"). They are importable modules, not scripts -- this package installs nothing under /usr/bin -- and the shebangs produced 56 rpmlint non-executable-script errors. brp-mangle-shebangs rewrites shebangs but does not remove them, so it happens in %%prep, guarded by a >= 20 floor before and a zero-remaining assertion after. - No .git directory and no prebuilt binaries in this sdist, unlike the NaviNIBS addons and NaviNIBS_Cobot. %%install asserts both anyway.