From 0533ef699549afe53473257d77d6a74e3cc90e95 Mon Sep 17 00:00:00 2001 From: Rodolfo Olivieri Date: Tue, 2 Dec 2025 10:53:27 -0300 Subject: [PATCH] Add packit config and rpm specfile Packit integration will help us to keep generating new copr builds from each pull request and main push automatically. --- .gitignore | 2 + .packit.yaml | 69 ++++++++++++++++++++++++ packaging/constraints.txt | 3 ++ packaging/linux-mcp-server.spec | 96 +++++++++++++++++++++++++++++++++ setup.py | 53 ++++++++++++++++++ 5 files changed, 223 insertions(+) create mode 100644 .packit.yaml create mode 100644 packaging/constraints.txt create mode 100644 packaging/linux-mcp-server.spec create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 35a62742..812595f4 100644 --- a/.gitignore +++ b/.gitignore @@ -60,4 +60,6 @@ config.sh .claude/settings.local.json CLAUDE.local.md +<<<<<<< HEAD +prepare_sources_result*/ diff --git a/.packit.yaml b/.packit.yaml new file mode 100644 index 00000000..79670317 --- /dev/null +++ b/.packit.yaml @@ -0,0 +1,69 @@ +# See the documentation for more information: +# https://packit.dev/docs/configuration/ + +specfile_path: packaging/linux-mcp-server.spec + +# name in upstream package repository or registry (e.g. in PyPI) +upstream_package_name: linux-mcp-server +# downstream (Fedora) RPM package name +downstream_package_name: linux-mcp-server +upstream_project_url: https://github.com/rhel-lightspeed/linux-mcp-server + +srpm_build_deps: + - python3-pip + - uv + +jobs: + # Build RPMs for each pull request + - job: copr_build + trigger: pull_request + owner: "@rhel-lightspeed" + project: linux-mcp-server + targets: + - rhel-9-x86_64 + - rhel-10-x86_64 + actions: + # Anchors are defined inline here; the main-branch job references them. + post-upstream-clone: &vendor_actions + # Resolve dependencies separately for each target Python version. + # Python 3.9 (RHEL 9) and 3.12 (RHEL 10) have different dependency + # constraints (e.g. some packages drop Python 3.9 support in newer + # versions). + - "uv pip compile pyproject.toml --python-version 3.9 -c packaging/constraints.txt --no-header --no-annotate -o requirements-39.txt" + - "uv pip compile pyproject.toml --python-version 3.12 -c packaging/constraints.txt --no-header --no-annotate -o requirements-312.txt" + # Download binary wheels for each Python version. + # --no-deps is required because pip evaluates environment markers using + # the host Python (e.g. 3.14 on Fedora), not the target version set by + # --python-version, causing false dependency conflicts. + - "pip3 download --no-deps --dest vendor_wheels --only-binary=:all: --python-version 3.9 --abi cp39 --implementation cp --platform manylinux_2_17_x86_64 --platform manylinux_2_28_x86_64 -r requirements-39.txt" + - "pip3 download --no-deps --dest vendor_wheels --only-binary=:all: --python-version 3.12 --abi cp312 --implementation cp --platform manylinux_2_17_x86_64 --platform manylinux_2_28_x86_64 -r requirements-312.txt" + # Bundle requirements files so the spec can select the right one at + # build time based on the target Python version. + - "cp requirements-39.txt requirements-312.txt vendor_wheels/" + # Place the tarball in the spec directory so rpmbuild includes it in + # the SRPM alongside Source0. + - tar czf packaging/vendor-wheels.tar.gz vendor_wheels/ + # Rewrite Source1 from the GitHub release URL to a local filename so + # packit uses the tarball generated above instead of downloading it. + fix-spec-file: + - "sed -i 's|^Source1:.*|Source1: vendor-wheels.tar.gz|' packaging/linux-mcp-server.spec" + # Do not get the version from a tag (git describe) but from the spec file. + get-current-version: &get_version + - grep -oP '^Version:\s+\K\S+' packaging/linux-mcp-server.spec + + # Build RPMs for main branch + - job: copr_build + trigger: commit + branch: main + owner: "@rhel-lightspeed" + project: linux-mcp-server + targets: + - rhel-9-x86_64 + - rhel-10-x86_64 + actions: + post-upstream-clone: *vendor_actions + fix-spec-file: + - "sed -i 's|^Source1:.*|Source1: vendor-wheels.tar.gz|' packaging/linux-mcp-server.spec" + # Bump release so main builds have higher NVR than PR builds. + - rpmdev-bumpspec --comment='latest upstream build' ./packaging/linux-mcp-server.spec + get-current-version: *get_version diff --git a/packaging/constraints.txt b/packaging/constraints.txt new file mode 100644 index 00000000..aaac8f9e --- /dev/null +++ b/packaging/constraints.txt @@ -0,0 +1,3 @@ +# Constraints for vendored wheel downloads. +# Add version pins here when a dependency drops support for a target +# Python version or stops publishing compatible prebuilt wheels. diff --git a/packaging/linux-mcp-server.spec b/packaging/linux-mcp-server.spec new file mode 100644 index 00000000..6ae3f449 --- /dev/null +++ b/packaging/linux-mcp-server.spec @@ -0,0 +1,96 @@ +# Disable debuginfo/debugsource generation. Vendored pydantic_core ships +# prebuilt .so files that lack debug sources, causing empty debugsourcefiles.list. +%global debug_package %{nil} +%define python_package_src linux_mcp_server + +Name: linux-mcp-server +Version: 0.1.0a3 +Release: 1%{?dist} +Summary: MCP server for read-only Linux system administration + +License: Apache-2.0 AND GPL-3.0-or-later +URL: https://github.com/rhel-lightspeed/linux-mcp-server +Source: %{url}/archive/v%{version}/linux-mcp-server-%{version}.tar.gz +# Vendored dependency wheels generated by pip download from requirements.txt. +# Contains wheels for all target Python versions (cp39, cp312) and x86_64. +# Shipped as a GitHub release artifact for tagged releases; generated by packit +# post-upstream-clone action for PR/main builds. +Source1: %{url}/releases/download/v%{version}/vendor-wheels.tar.gz + +# Not BuildArch: noarch because pydantic_core contains compiled .so files + +# Build dependencies +BuildRequires: python3-devel +BuildRequires: python3-pip +BuildRequires: python3-setuptools +BuildRequires: python3-wheel + +# Not needed after RHEL 10 as it is native in Python 3.11+ +%if 0%{?rhel} && 0%{?rhel} < 10 +BuildRequires: python3-tomli +Requires: python3-tomli +%endif + +%global _description %{expand: +MCP server for read-only Linux system administration, diagnostics, and troubleshooting.} + +# All Python dependencies are vendored in _vendor. Suppress the auto-generated +# python3.Xdist(...) Requires so RPM does not demand them as system packages. +%{?python_disable_dependency_generator} + +%description %_description + + +%prep +%autosetup -p1 + +# Remove pytest options not available in RHEL +sed -i '/^addopts = \[/,/^\]/d' pyproject.toml +sed -i '/^asyncio_mode = /d' pyproject.toml + +# Remove license-files as this is a conflicting property with older setuptools +sed -i '/^license-files = /d' pyproject.toml + +# Switch build backend from hatchling to setuptools (hatchling not available on RHEL) +sed -i 's/requires = \["hatchling", "hatch-vcs"\]/requires = ["setuptools", "wheel"]/' pyproject.toml +sed -i 's/build-backend = "hatchling.build"/build-backend = "setuptools.build_meta"/' pyproject.toml + +# Remove dynamic version (set by hatch-vcs) and set static version +sed -i '/^dynamic = \["version"\]/d' pyproject.toml +sed -i '/^name = "linux-mcp-server"/a version = "%{version}"' pyproject.toml + +# Add setuptools package discovery and include .so from vendored deps +cat >> pyproject.toml <<'EOF' + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +"*" = ["*.so"] +EOF + +# Install vendored dependency wheels into the _vendor directory so they are +# included in the wheel built during %%build. The tarball contains wheels for +# multiple Python versions (cp39, cp312). A per-version requirements file +# selects the correct packages and pip picks the compatible wheel. +tar xzf %{SOURCE1} +%{python3} -m pip install --no-deps --no-index \ + --find-links vendor_wheels --target src/%{python_package_src}/_vendor \ + -r vendor_wheels/requirements-%{python3_version_nodots}.txt + +%build +%py3_build_wheel + +%install +%py3_install_wheel %{python_package_src}-%{version}-py3-none-any.whl + +%files +%license LICENSE licenses/GPL-3.0.txt +%doc README.md + +%{_bindir}/linux-mcp-server +%{python3_sitelib}/%{python_package_src}/ +%{python3_sitelib}/%{python_package_src}-%{version}.dist-info/ + +%changelog +%autochangelog diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..99b6d555 --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +# RHEL 9 ships setuptools < 61 which cannot read the [project] table from +# pyproject.toml (PEP 621). Read pyproject metadata here and pass it to +# setup() so older setuptools can still build the package correctly. +import sys + +from setuptools import find_packages +from setuptools import setup + + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + +pyproject_settings = {} +with open("pyproject.toml", "rb") as f: + pyproject_settings = tomllib.load(f) + +project = pyproject_settings["project"] + +entry_points: dict[str, list[str]] = {"console_scripts": []} +for script_name, script_path in project.get("scripts", {}).items(): + entry_points["console_scripts"].append(f"{script_name} = {script_path}") + +long_description = None +with open( + project["readme"], mode="r", encoding="utf-8" +) as handler: + long_description = handler.read() + +authors = project.get("authors", [{}]) +author_name = authors[0].get("name", "") if authors else "" +author_email = authors[0].get("email", "") if authors else "" + +setup( + name=project["name"], + version=project.get("version", "0.0.0"), + author=author_name, + author_email=author_email, + description=project["description"], + long_description=long_description, + long_description_content_type="text/markdown", + url=project.get("urls", {}).get("Source code", ""), + package_dir={"": "src"}, + packages=find_packages(where="src"), + # Include compiled extensions (.so) from vendored packages like pydantic_core. + # setuptools only includes .py files by default. + package_data={"": ["*.so"]}, + install_requires=project.get("dependencies", []), + entry_points=entry_points, + classifiers=project.get("classifiers", []), + python_requires=project.get("requires-python", ">=3.10"), +)