Skip to content

Commit c8f5a3f

Browse files
authored
Packaging for Fedora (#422)
* create script to build rpm with docker #340 Signed-off-by: Chin Yeung Li <[email protected]> * remove the hyphen replacement #340 Signed-off-by: Chin Yeung Li <[email protected]> * Update script to put generated package under dist/rpmbuild/ #340 Signed-off-by: Chin Yeung Li <[email protected]> * Reformat the code to comply with Ruff's check #340 Signed-off-by: Chin Yeung Li <[email protected]> * Moved the build_rpm_docker.py to etc/scripts #340 Signed-off-by: Chin Yeung Li <[email protected]> * Update comment on how to install the generated RPM #340 Signed-off-by: Chin Yeung Li <[email protected]> * Update to handle built dependencies #340 Signed-off-by: Chin Yeung Li <[email protected]> * Update packaging script for Fedora #340 - Removed building funcparserlib - Changed BuildArch from noarch to x86_64 - Resolved dependencies issues Signed-off-by: Chin Yeung Li <[email protected]> * Specify to use python313 to build as this is the version required #340 Signed-off-by: Chin Yeung Li <[email protected]> --------- Signed-off-by: Chin Yeung Li <[email protected]>
1 parent b6f8ec8 commit c8f5a3f

File tree

1 file changed

+299
-0
lines changed

1 file changed

+299
-0
lines changed

etc/scripts/build_rpm_docker.py

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Use Docker container to build an RPM.
4+
Using Docker approach to ensure a consistent and isolated build environment.
5+
6+
Requirement:
7+
- toml
8+
- Docker
9+
10+
To install the required Python packages, run:
11+
12+
pip install toml
13+
14+
To install Docker, follow the instructions at:
15+
https://docs.docker.com/get-docker/
16+
17+
To run the script:
18+
19+
python build_rpm_docker.py
20+
21+
This script will generate the RPM package files and place them in the
22+
dist/rpmbuild/ directory.
23+
24+
Once the RPM package is generated, one can install it using:
25+
26+
sudo rpm -i /path/to/<dejacode>.rpm
27+
OR
28+
sudo dnf install /path/to/<dejacode>.rpm
29+
OR
30+
sudo yum install /path/to/<dejacode>.rpm
31+
32+
Replace the above path with the actual path to the generated RPM file.
33+
34+
Run the binary directly
35+
36+
dejacode
37+
"""
38+
39+
import os
40+
import shutil
41+
import subprocess
42+
import sys
43+
from pathlib import Path
44+
45+
import toml
46+
47+
48+
def build_rpm_with_docker():
49+
# Load the pyproject.toml file
50+
with open("pyproject.toml") as f:
51+
project = toml.load(f)["project"]
52+
53+
pkg_name = project["name"]
54+
# Insert "python3-"" prefix that follows a common convention for Python RPMs
55+
rpm_name = f"python3-{pkg_name.lower()}"
56+
rpm_version = project["version"].replace("-dev", "~dev")
57+
58+
# Generate requirements for RPM - exclude packages installed from GitHub
59+
dependencies = project["dependencies"]
60+
61+
filtered_dependencies = [
62+
dep
63+
for dep in dependencies
64+
if "django-rest-hooks" not in dep and "django-notifications-patched" not in dep
65+
]
66+
67+
# Create a requirements.txt content for installation
68+
requirements_content = "\n".join(filtered_dependencies)
69+
70+
docker_cmd = [
71+
"docker",
72+
"run",
73+
"--rm",
74+
"-v",
75+
f"{os.getcwd()}:/workspace",
76+
"-w",
77+
"/workspace",
78+
"fedora:42",
79+
"/bin/bash",
80+
"-c",
81+
f"""set -ex
82+
# Install build dependencies
83+
dnf install -y rpm-build python3.13-devel python3.13-setuptools python3.13-wheel \\
84+
python3.13-build python3.13-pip python3.13-virtualenv curl gcc openldap-devel git
85+
86+
# Clean up and build wheel
87+
rm -rf build dist
88+
python3.13 -m build --wheel
89+
90+
# Get the wheel file name
91+
WHEEL_FILE=$(ls dist/*.whl)
92+
if [ -z "$WHEEL_FILE" ]; then
93+
echo "Error: No wheel file found in dist/." >&2
94+
exit 1
95+
fi
96+
WHEEL_FILENAME=$(basename "$WHEEL_FILE")
97+
98+
# Creates the standard directory structure required by rpmbuild
99+
mkdir -p dist/rpmbuild/{{BUILD,RPMS,SOURCES,SPECS,SRPMS}}
100+
mv "$WHEEL_FILE" dist/rpmbuild/SOURCES/
101+
102+
# Create requirements.txt in SOURCES
103+
cat > dist/rpmbuild/SOURCES/requirements.txt << 'REQ_EOF'
104+
{requirements_content}
105+
REQ_EOF
106+
107+
# Get the changelog date
108+
CHANGELOG_DATE=$(date '+%a %b %d %Y')
109+
110+
# Create source tarball
111+
tar czf dist/rpmbuild/SOURCES/{rpm_name}-{rpm_version}.tar.gz \\
112+
--transform "s,^,/{rpm_name}-{rpm_version}/," \\
113+
-C /workspace \\
114+
--exclude build --exclude=.git --exclude=dist --exclude=*.pyc --exclude=__pycache__ .
115+
116+
# Generate spec file
117+
cat > dist/rpmbuild/SPECS/{rpm_name}.spec << EOF
118+
Name: {rpm_name}
119+
Version: {rpm_version}
120+
Release: 1%{{?dist}}
121+
Summary: {
122+
project.get(
123+
"description",
124+
"Automate open source license compliance and ensure supply chain integrity",
125+
)
126+
}
127+
128+
License: {project.get("license", "AGPL-3.0-only")}
129+
URL: {
130+
project.get("urls", "").get("Homepage", "https://github.com/aboutcode-org/dejacode")
131+
}
132+
Source0: {rpm_name}-{rpm_version}.tar.gz
133+
Source1: requirements.txt
134+
Source2: $WHEEL_FILENAME
135+
136+
BuildArch: x86_64
137+
BuildRequires: python3.13-devel python3.13-virtualenv gcc openldap-devel git
138+
139+
# Runtime dependencies
140+
Requires: git
141+
Requires: python3.13
142+
Requires: postgresql
143+
Requires: postgresql-devel
144+
Requires: openldap
145+
146+
# Disable automatic debug package generation and file checking
147+
%global debug_package %{{nil}}
148+
%global __check_files %{{nil}}
149+
%global _enable_debug_package 0
150+
151+
# Only disable python bytecompilation which breaks virtualenvs
152+
%global __brp_python_bytecompile %{{nil}}
153+
154+
# Keep shebang mangling disabled for virtualenv
155+
%global __os_install_post %(echo '%{{__os_install_post}}' | \
156+
sed -e 's!/usr/lib/rpm/redhat/brp-mangle-shebangs!!g')
157+
%global __brp_mangle_shebangs %{{nil}}
158+
159+
AutoReqProv: no
160+
161+
%description
162+
{
163+
project.get(
164+
"description",
165+
"Automate open source license compliance and ensure supply chain integrity",
166+
)
167+
}
168+
169+
%prep
170+
%setup -q
171+
172+
%build
173+
174+
%install
175+
rm -rf %{{buildroot}}
176+
# Create directories
177+
mkdir -p %{{buildroot}}/opt/%{{name}}/venv
178+
mkdir -p %{{buildroot}}/usr/bin
179+
mkdir -p %{{buildroot}}/opt/%{{name}}/src
180+
181+
# Create virtual environment in a temporary location first
182+
mkdir -p /tmp/venv_build
183+
python3.13 -m venv /tmp/venv_build --copies
184+
185+
cd %{{_sourcedir}}
186+
/tmp/venv_build/bin/python -m pip install --upgrade pip
187+
188+
# Install system dependencies for psycopg
189+
dnf install -y postgresql-devel
190+
191+
# Install non-PyPI dependencies
192+
/tmp/venv_build/bin/python -m pip install \\
193+
https://github.com/aboutcode-org/django-rest-hooks/releases/download/1.6.1/django_rest_hooks-1.6.1-py2.py3-none-any.whl
194+
/tmp/venv_build/bin/python -m pip install \\
195+
https://github.com/dejacode/django-notifications-patched/archive/refs/tags/2.0.0.tar.gz
196+
197+
# Install the main package
198+
/tmp/venv_build/bin/python -m pip install -r requirements.txt
199+
200+
# Install psycopg2-binary for compatibility
201+
/tmp/venv_build/bin/python -m pip install psycopg2-binary
202+
203+
# Install dejacode wheel
204+
/tmp/venv_build/bin/python -m pip install %{{_sourcedir}}/$WHEEL_FILENAME
205+
206+
# Extract source code for tests
207+
cd %{{_sourcedir}}
208+
tar xzf {rpm_name}-{rpm_version}.tar.gz
209+
cp -r {rpm_name}-{rpm_version}/* %{{buildroot}}/opt/%{{name}}/src/
210+
211+
# Clean up temporary virtualenv
212+
find /tmp/venv_build -name "*.pyc" -delete
213+
find /tmp/venv_build -name "__pycache__" -type d -exec rm -rf {{}} + 2>/dev/null || true
214+
215+
# Copy the completed virtual environment to the final location
216+
cp -r /tmp/venv_build/* %{{buildroot}}/opt/%{{name}}/venv/
217+
218+
# Clean up temporary virtual environment
219+
rm -rf /tmp/venv_build
220+
221+
# Fix shebang
222+
for script in %{{buildroot}}/opt/%{{name}}/venv/bin/*; do
223+
if [ -f "\\$script" ] && head -1 "\\$script" | grep -q "^#!"; then
224+
# Use sed to safely replace only the first line
225+
sed -i '1s|.*|#!/opt/%{{name}}/venv/bin/python3|' "\\$script"
226+
fi
227+
done
228+
229+
# Remove ONLY pip and wheel binaries
230+
rm -f %{{buildroot}}/opt/%{{name}}/venv/bin/pip*
231+
rm -f %{{buildroot}}/opt/%{{name}}/venv/bin/wheel
232+
233+
# Ensure executables have proper permissions
234+
find %{{buildroot}}/opt/%{{name}}/venv/bin -type f -exec chmod 755 {{}} \\;
235+
236+
# Create wrapper script with PYTHONPATH for tests
237+
cat > %{{buildroot}}/usr/bin/dejacode << 'WRAPPER_EOF'
238+
#!/bin/sh
239+
export PYTHONPATH="/opt/%{{name}}/src:/opt/%{{name}}/venv/lib/python3.13/site-packages"
240+
cd "/opt/%{{name}}/src"
241+
/opt/%{{name}}/venv/bin/dejacode "\\$@"
242+
WRAPPER_EOF
243+
chmod 755 %{{buildroot}}/usr/bin/dejacode
244+
245+
%files
246+
%defattr(-,root,root,-)
247+
%dir /opt/%{{name}}
248+
/opt/%{{name}}/venv/
249+
/opt/%{{name}}/src/
250+
/usr/bin/dejacode
251+
252+
%changelog
253+
* $CHANGELOG_DATE {project.get("authors", [{}])[0].get("name", "nexB Inc.")} - {rpm_version}-1
254+
- {
255+
project.get("urls", "").get(
256+
"Changelog", "https://github.com/aboutcode-org/dejacode/blob/main/CHANGELOG.rst"
257+
)
258+
}
259+
EOF
260+
261+
# Build RPM with only specific BRP processing disabled
262+
cd dist/rpmbuild && rpmbuild \\
263+
--define "_topdir $(pwd)" \\
264+
--define "__check_files /bin/true" \\
265+
--define "__brp_python_bytecompile /bin/true" \\
266+
-bb SPECS/{rpm_name}.spec
267+
268+
269+
# Fix permissions for Windows host
270+
chmod -R u+rwX /workspace/dist/rpmbuild
271+
""",
272+
]
273+
274+
try:
275+
subprocess.run(docker_cmd, check=True, shell=False) # noqa: S603
276+
# Verify the existence of the .rpm
277+
rpm_file = next(Path("dist/rpmbuild/RPMS/x86_64").glob("*.rpm"), None)
278+
if rpm_file:
279+
print(f"\nSuccess! RPM built: {rpm_file}")
280+
else:
281+
print("Error: RPM not found in dist/rpmbuild/RPMS/x86_64/", file=sys.stderr)
282+
sys.exit(1)
283+
except subprocess.CalledProcessError as e:
284+
print(f"Build failed: {e}", file=sys.stderr)
285+
sys.exit(1)
286+
287+
288+
if __name__ == "__main__":
289+
# Check if "docker" is available
290+
if not shutil.which("docker"):
291+
print("Error: Docker not found. Please install Docker first.", file=sys.stderr)
292+
sys.exit(1)
293+
294+
# Get the directory where the current script is located (which is located in etc/scripts)
295+
script_dir = Path(__file__).parent.resolve()
296+
# Go up two levels from etc/scripts/
297+
project_root = script_dir.parent.parent
298+
os.chdir(project_root)
299+
build_rpm_with_docker()

0 commit comments

Comments
 (0)