Skip to content

Commit b6f8ec8

Browse files
authored
Packaging for Debian (#424)
* Add script to package to debian with docker #341 Signed-off-by: Chin Yeung Li <[email protected]> * Update script to put generated package under dist/debian/ #341 Signed-off-by: Chin Yeung Li <[email protected]> * Reformat the code to comply with Ruff's check #341 Signed-off-by: Chin Yeung Li <[email protected]> * Move the build_deb_docker.py under etc/scripts and update comment #341 Signed-off-by: Chin Yeung Li <[email protected]> * Update packaging script for Debian #341 Signed-off-by: Chin Yeung Li <[email protected]> --------- Signed-off-by: Chin Yeung Li <[email protected]>
1 parent c987d00 commit b6f8ec8

File tree

1 file changed

+236
-0
lines changed

1 file changed

+236
-0
lines changed

etc/scripts/build_deb_docker.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Use Docker container to build a Debian package.
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_deb_docker.py
20+
21+
This script will generate the Debian package files and the python wheel and
22+
place them in the dist/debian/ directory.
23+
24+
Once the debian package is generated, one can install it using:
25+
26+
sudo apt install ./<package>.deb
27+
28+
Note: The ./ is important - it tells apt to install from a local file
29+
rather than searching repositories.
30+
Replace the above path with the actual path to the generated debian file.
31+
32+
Run the binary directly
33+
34+
dejacode
35+
"""
36+
37+
import os
38+
import shutil
39+
import subprocess
40+
import sys
41+
from pathlib import Path
42+
43+
import toml
44+
45+
46+
def build_deb_with_docker():
47+
# Load the pyproject.toml file
48+
with open("pyproject.toml") as f:
49+
project = toml.load(f)["project"]
50+
51+
pkg_name = project["name"]
52+
# Insert "python3-"" prefix that follows a common convention
53+
deb_name = f"python3-{pkg_name.lower()}"
54+
55+
# Debian version conventions replace hyphens with tildes
56+
deb_version = project["version"].replace("-dev", "~dev")
57+
58+
# Get all dependencies
59+
dependencies = project.get("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+
docker_cmd = [
68+
"docker",
69+
"run",
70+
"--rm",
71+
"-v",
72+
f"{os.getcwd()}:/workspace",
73+
"-w",
74+
"/workspace",
75+
"ubuntu:24.04",
76+
"/bin/bash",
77+
"-c",
78+
f"""set -ex
79+
# Install build dependencies
80+
apt-get update
81+
apt-get install -y debhelper dh-python devscripts build-essential \
82+
libsasl2-dev libldap2-dev libssl-dev libpq-dev
83+
84+
# Install Python 3.13 from deadsnakes PPA
85+
apt-get install -y software-properties-common
86+
add-apt-repository -y ppa:deadsnakes/ppa
87+
apt-get update
88+
apt-get install -y python3.13 python3.13-dev python3.13-venv
89+
90+
# Create and activate virtual environment with Python 3.13 using --copies
91+
python3.13 -m venv /opt/{deb_name} --copies
92+
. /opt/{deb_name}/bin/activate
93+
94+
# Upgrade pip to latest version and ensure setuptools is available
95+
pip install --upgrade pip setuptools wheel
96+
97+
# Clean previous build artifacts
98+
rm -rf build/
99+
100+
# Install non-PyPI dependencies
101+
pip install https://github.com/aboutcode-org/django-rest-hooks/releases/download/1.6.1/django_rest_hooks-1.6.1-py2.py3-none-any.whl
102+
pip install https://github.com/dejacode/django-notifications-patched/archive/refs/tags/2.0.0.tar.gz
103+
104+
# Install dependencies directly
105+
{" && ".join([f'pip install "{dep}"' for dep in filtered_dependencies])}
106+
107+
# Install build tool
108+
pip install build
109+
110+
# Build the wheel
111+
python3.13 -m build --wheel
112+
113+
# Install the package and all remaining dependencies
114+
WHEEL_FILE=$(ls dist/*.whl)
115+
116+
# Install the main package
117+
pip install "$WHEEL_FILE"
118+
119+
# Copy source code to /opt/{deb_name}/src
120+
mkdir -p /opt/{deb_name}/src
121+
cp -r /workspace/* /opt/{deb_name}/src/ 2>/dev/null || true
122+
rm -rf /opt/{deb_name}/src/dist /opt/{deb_name}/src/build 2>/dev/null || true
123+
124+
# Fix shebangs in the virtual environment to use absolute paths
125+
for script in /opt/{deb_name}/bin/*; do
126+
if [ -f "$script" ] && head -1 "$script" | grep -q "^#!"; then
127+
# Use sed to safely replace only the first line with absolute path
128+
sed -i '1s|.*|#!/opt/{deb_name}/bin/python3|' "$script"
129+
fi
130+
done
131+
132+
# Remove pip and wheel to reduce package size
133+
rm -f /opt/{deb_name}/bin/pip* /opt/{deb_name}/bin/wheel
134+
135+
# Ensure all scripts are executable
136+
chmod -R 755 /opt/{deb_name}/bin/
137+
138+
# Create wrapper script (like in RPM) instead of direct symlink
139+
cat > /opt/{deb_name}/bin/dejacode-wrapper << 'WRAPPER_EOF'
140+
#!/bin/bash
141+
export PYTHONPATH="/opt/{deb_name}/src:/opt/{deb_name}/lib/python3.13/site-packages"
142+
cd "/opt/{deb_name}/src"
143+
exec "/opt/{deb_name}/bin/dejacode" "$@"
144+
WRAPPER_EOF
145+
chmod 755 /opt/{deb_name}/bin/dejacode-wrapper
146+
147+
# Create temporary directory for package building
148+
TEMP_DIR=$(mktemp -d)
149+
PKG_DIR="$TEMP_DIR/{deb_name}-{deb_version}"
150+
mkdir -p "$PKG_DIR"
151+
152+
# Copy the installed package files
153+
mkdir -p "$PKG_DIR/opt/"
154+
cp -r /opt/{deb_name} "$PKG_DIR/opt/"
155+
156+
# Create DEBIAN control file
157+
mkdir -p "$PKG_DIR/DEBIAN"
158+
cat > "$PKG_DIR/DEBIAN/control" << EOF
159+
Package: {deb_name}
160+
Version: {deb_version}
161+
Architecture: all
162+
Maintainer: {project.get("authors", [{}])[0].get("name", "nexB Inc.")}
163+
Description: {
164+
project.get(
165+
"description",
166+
"Automate open source license compliance andensure supply chain integrity",
167+
)
168+
}
169+
Depends: python3.13, git, libldap2 | libldap-2.5-0, libsasl2-2, libssl3 | libssl3t64, libpq5
170+
Section: python
171+
Priority: optional
172+
Homepage: {project.get("urls", "").get("Homepage", "https://github.com/aboutcode-org/dejacode")}
173+
EOF
174+
175+
# Create postinst script to setup symlinks
176+
cat > "$PKG_DIR/DEBIAN/postinst" << 'POSTINST'
177+
#!/bin/bash
178+
# Create symlinks for binaries - use the wrapper script
179+
if [ -f "/opt/{deb_name}/bin/dejacode-wrapper" ]; then
180+
ln -sf "/opt/{deb_name}/bin/dejacode-wrapper" "/usr/local/bin/dejacode"
181+
fi
182+
183+
# Ensure proper permissions
184+
chmod -R 755 /opt/{deb_name}/bin/
185+
POSTINST
186+
chmod 755 "$PKG_DIR/DEBIAN/postinst"
187+
188+
# Create prerm script to clean up symlinks
189+
cat > "$PKG_DIR/DEBIAN/prerm" << 'PRERM'
190+
#!/bin/bash
191+
# Remove symlinks
192+
rm -f "/usr/local/bin/dejacode"
193+
PRERM
194+
chmod 755 "$PKG_DIR/DEBIAN/prerm"
195+
196+
# Build the .deb package
197+
mkdir -p dist/debian
198+
dpkg-deb --build "$PKG_DIR" "dist/debian/{deb_name}_{deb_version}_all.deb"
199+
200+
# Move wheel to dist/debian/
201+
mv dist/*.whl dist/debian/
202+
203+
# Clean up
204+
rm -rf "$TEMP_DIR"
205+
206+
# Fix permissions for Windows host
207+
chmod -R u+rwX dist/debian/
208+
""",
209+
]
210+
211+
try:
212+
subprocess.run(docker_cmd, check=True, shell=False) # noqa: S603
213+
# Verify the existence of the .deb
214+
deb_file = next(Path("dist/debian").glob("*.deb"), None)
215+
if deb_file:
216+
print(f"\nSuccess! Debian package built: {deb_file}")
217+
else:
218+
print("Error: Debian package not found in dist/debian/", file=sys.stderr)
219+
sys.exit(1)
220+
except subprocess.CalledProcessError as e:
221+
print(f"Build failed: {e}", file=sys.stderr)
222+
sys.exit(1)
223+
224+
225+
if __name__ == "__main__":
226+
# Check if "docker" is available
227+
if not shutil.which("docker"):
228+
print("Error: Docker not found. Please install Docker first.", file=sys.stderr)
229+
sys.exit(1)
230+
231+
# Get the directory where the current script is located (which is located in etc/scripts)
232+
script_dir = Path(__file__).parent.resolve()
233+
# Go up two levels from etc/scripts/
234+
project_root = script_dir.parent.parent
235+
os.chdir(project_root)
236+
build_deb_with_docker()

0 commit comments

Comments
 (0)