Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,6 @@ config.sh

.claude/settings.local.json
CLAUDE.local.md
<<<<<<< HEAD

prepare_sources_result*/
69 changes: 69 additions & 0 deletions .packit.yaml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions packaging/constraints.txt
Original file line number Diff line number Diff line change
@@ -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.
96 changes: 96 additions & 0 deletions packaging/linux-mcp-server.spec
Original file line number Diff line number Diff line change
@@ -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
53 changes: 53 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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"),
)
Loading