Skip to content

Commit bbea903

Browse files
authored
Merge pull request #8 from qount25/deb
Building DEB packages with podman & pbuilder
2 parents 48492ae + 3073f73 commit bbea903

25 files changed

+744
-83
lines changed

exe/pgpm

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -101,35 +101,53 @@ module Pgpm
101101
exit(1)
102102
end
103103

104-
unless os.is_a?(Pgpm::OS::RedHat)
105-
puts "#{os.name} is not a supported OS at this moment"
106-
exit(1)
107-
end
108-
puts "Building #{pkgs.map { |p| "#{p.name}@#{p.version}" }.join(", ")} for Postgres #{matching_pgver}"
109-
selected_pgdist = Postgres::RedhatBasedPgdg.new(matching_pgver.to_s)
110-
111-
os.with_scope do
112-
arch.with_scope do
113-
selected_pgdist.with_scope do
114-
pkgs = pkgs.flat_map(&:topologically_ordered_with_dependencies).uniq.reject(&:contrib?)
115-
116-
b = pkgs.reduce(nil) do |c, p|
117-
if p.broken?
118-
puts "Can't build a broken package #{p.name}@#{p.version}"
119-
exit(1)
104+
if os.is_a? Pgpm::OS::Debian
105+
puts "Building #{pkgs.map { |p| "#{p.name}@#{p.version}" }.join(", ")} for Postgres #{matching_pgver}"
106+
selected_pgdist = Postgres::RedhatBasedPgdg.new(matching_pgver.to_s)
107+
108+
os.with_scope do
109+
arch.with_scope do
110+
selected_pgdist.with_scope do
111+
spec = nil
112+
pkgs.reduce(nil) do |_c, p|
113+
p = Pgpm::ScopedObject.new(p, os, arch)
114+
spec = p.to_deb_spec
120115
end
121-
p = Pgpm::ScopedObject.new(p, os, arch)
122-
spec = p.to_rpm_spec
123-
builder = Pgpm::RPM::Builder.new(spec)
124-
src_builder = builder.source_builder
125-
p = c.nil? ? src_builder : c.and_then(src_builder)
126-
p.and_then(builder.versionless_builder)
116+
builder = Pgpm::Deb::Builder.new(spec)
117+
builder.build
127118
end
119+
end
120+
end
121+
elsif os.is_a? Pgpm::OS::RedHat
122+
puts "Building #{pkgs.map { |p| "#{p.name}@#{p.version}" }.join(", ")} for Postgres #{matching_pgver}"
123+
selected_pgdist = Postgres::RedhatBasedPgdg.new(matching_pgver.to_s)
124+
125+
os.with_scope do
126+
arch.with_scope do
127+
selected_pgdist.with_scope do
128+
pkgs = pkgs.flat_map(&:topologically_ordered_with_dependencies).uniq.reject(&:contrib?)
129+
130+
b = pkgs.reduce(nil) do |c, p|
131+
if p.broken?
132+
puts "Can't build a broken package #{p.name}@#{p.version}"
133+
exit(1)
134+
end
135+
p = Pgpm::ScopedObject.new(p, os, arch)
136+
spec = p.to_rpm_spec
137+
builder = Pgpm::RPM::Builder.new(spec)
138+
src_builder = builder.source_builder
139+
p = c.nil? ? src_builder : c.and_then(src_builder)
140+
p.and_then(builder.versionless_builder)
141+
end
128142

129-
srpms = b.call
130-
Pgpm::RPM::Builder.builder(srpms).call
143+
srpms = b.call
144+
Pgpm::RPM::Builder.builder(srpms).call
145+
end
131146
end
132147
end
148+
else
149+
puts "#{os.name} is not a supported OS at this moment"
150+
exit(1)
133151
end
134152
end
135153

lib/pgpm/deb/Dockerfile

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# syntax = docker/dockerfile:experimental
2+
3+
# IMPORTANT: build it this way to allow for privileged execution
4+
#
5+
# Docker daemon config should have the entitlement
6+
# ```json
7+
# { "builder": {"Entitlements": {"security-insecure": true }} }
8+
# ```
9+
# ```
10+
# DOCKER_BUILDKIT=1 docker build --allow security.insecure -t IMAGE_NAME /path/to/pgpm
11+
# ```
12+
13+
# This Dockerfile is used to build a Debian image, which includes pbuilder and
14+
# pbuilder chroot image with basic dependendencies needed for building most
15+
# packages already pre-installed.
16+
17+
FROM docker.io/library/debian
18+
19+
MAINTAINER PGPM Debian Maintainer [email protected]
20+
21+
VOLUME /proc
22+
ARG DEBIAN_FRONTEND=noninteractive
23+
RUN apt update
24+
RUN apt install -y build-essential pbuilder fakeroot fakechroot
25+
RUN echo 'MIRRORSITE=http://deb.debian.org/debian' > /etc/pbuilderrc
26+
RUN echo 'AUTO_DEBSIGN=${AUTO_DEBSIGN:-no}' > /root/.pbuilderrc
27+
RUN echo 'HOOKDIR=/var/cache/pbuilder/hooks' >> /root/.pbuilderrc
28+
RUN --security=insecure pbuilder create
29+
30+
COPY pbuilder_install_script.sh /root/pbuilder_install_script.sh
31+
RUN --security=insecure pbuilder execute --save-after-exec /root/pbuilder_install_script.sh

lib/pgpm/deb/builder.rb

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# frozen_string_literal: true
2+
3+
require "English"
4+
require "debug"
5+
6+
module Pgpm
7+
module Deb
8+
class Builder
9+
def initialize(spec)
10+
@spec = spec
11+
@container_name = "pgpm-debian_build-#{Time.now.to_i}_#{rand(10_000)}"
12+
@pgpm_dir = Dir.mktmpdir
13+
end
14+
15+
def build
16+
pull_image
17+
start_container
18+
patch_pbuilder
19+
20+
prepare_versioned_source
21+
generate_deb_src_files(:versioned)
22+
run_build(:versioned)
23+
copy_build_from_container(:versioned)
24+
25+
prepare_default_source
26+
generate_deb_src_files(:default)
27+
run_build(:default)
28+
copy_build_from_container(:default)
29+
30+
cleanup
31+
end
32+
33+
private
34+
35+
# Depends on postgres version and arch
36+
def image_name
37+
"quay.io/qount25/pgpm-debian-pg#{@spec.package.postgres_major_version}-#{@spec.arch}"
38+
end
39+
40+
def prepare_versioned_source
41+
puts "Preparing build..."
42+
puts " Creating container dir structure..."
43+
Dir.mkdir "#{@pgpm_dir}/source-versioned"
44+
Dir.mkdir "#{@pgpm_dir}/out"
45+
46+
puts " Downloading and unpacking sources to #{@pgpm_dir}"
47+
48+
fn = nil
49+
@spec.sources.map do |src|
50+
srcfile = File.join(@pgpm_dir.to_s, src.name)
51+
File.write(srcfile, src.read)
52+
fn = src.name
53+
end
54+
55+
system("tar -xf #{@pgpm_dir}/#{fn} -C #{@pgpm_dir}/source-versioned/")
56+
FileUtils.remove("#{@pgpm_dir}/#{fn}")
57+
58+
untar_dir_entries = Dir.entries("#{@pgpm_dir}/source-versioned/").reject do |entry|
59+
[".", ".."].include?(entry)
60+
end
61+
62+
if untar_dir_entries.size == 1
63+
entry = untar_dir_entries[0]
64+
if File.directory?("#{@pgpm_dir}/source-versioned/#{entry}")
65+
FileUtils.mv "#{@pgpm_dir}/source-versioned/#{entry}", "#{@pgpm_dir}/"
66+
FileUtils.remove_dir "#{@pgpm_dir}/source-versioned/"
67+
FileUtils.mv "#{@pgpm_dir}/#{entry}", "#{@pgpm_dir}/source-versioned"
68+
end
69+
end
70+
71+
["prepare_artifacts.sh"].each do |f|
72+
script_fn = File.expand_path("#{__dir__}/scripts/#{f}")
73+
FileUtils.cp script_fn, "#{@pgpm_dir}/source-versioned/"
74+
end
75+
end
76+
77+
def prepare_default_source
78+
Dir.mkdir "#{@pgpm_dir}/source-default"
79+
80+
# 1. All pbuilder builds are in /var/cache/pbuilder/build. At this point
81+
# there's only one build, but we don't know what the directory is named
82+
# (the name is usually some numbers). So we just pick the first (and only)
83+
# entry at this location and this is our build dir.
84+
pbuilds_dir = "/var/cache/pbuilder/build"
85+
cmd = "ls -U #{pbuilds_dir} | head -1"
86+
build_dir = `podman exec #{@container_name} /bin/bash -c '#{cmd}'`.strip
87+
puts "BUILD DIR IS: #{pbuilds_dir}/#{build_dir}"
88+
89+
# 2. Determine the name of the .control file inside the versioned build
90+
deb_dir = "#{pbuilds_dir}/#{build_dir}/build/#{@spec.deb_pkg_name(:versioned)}-0/debian/#{@spec.deb_pkg_name(:versioned)}"
91+
control_fn = "#{deb_dir}/usr/share/postgresql/#{@spec.package.postgres_major_version}/extension/#{@spec.package.extension_name}--#{@spec.package.version}.control"
92+
93+
# 3. Copy .control file to the source-default dir
94+
puts "Copying #{control_fn} into /root/pgpm/source-default/"
95+
target_control_fn = "/root/pgpm/source-default/#{@spec.package.extension_name}.control"
96+
cmd = "cp #{control_fn} #{target_control_fn}"
97+
system("podman exec #{@container_name} /bin/bash -c '#{cmd}'")
98+
99+
["install_default_control.sh"].each do |fn|
100+
script_fn = File.expand_path("#{__dir__}/scripts/#{fn}")
101+
FileUtils.cp script_fn, "#{@pgpm_dir}/source-default/"
102+
end
103+
end
104+
105+
def pull_image
106+
puts "Checking if podman image exists..."
107+
# Check if image exists
108+
system("podman image exists #{image_name}")
109+
if $CHILD_STATUS.to_i.positive? # image doesn't exist -- pull image from a remote repository
110+
puts " No. Pulling image #{image_name}..."
111+
system("podman pull #{image_name}")
112+
else
113+
puts " Yes, image #{image_name} already exists! OK"
114+
end
115+
end
116+
117+
def generate_deb_src_files(pkg_type = :versioned)
118+
puts "Generating debian files..."
119+
Dir.mkdir "#{@pgpm_dir}/source-#{pkg_type}/debian"
120+
%i[changelog control copyright files rules].each do |f|
121+
puts " -> #{@pgpm_dir}/source-#{pkg_type}/debian/#{f}"
122+
File.write "#{@pgpm_dir}/source-#{pkg_type}/debian/#{f}", @spec.generate(f, pkg_type)
123+
end
124+
File.chmod 0o740, "#{@pgpm_dir}/source-#{pkg_type}/debian/rules" # rules file must be executable
125+
end
126+
127+
def start_container
128+
# podman create options
129+
create_opts = " -v #{@pgpm_dir}:/root/pgpm"
130+
create_opts += ":z" if selinux_enabled?
131+
create_opts += " --privileged --tmpfs /tmp"
132+
create_opts += " --name #{@container_name} #{image_name}"
133+
134+
puts " Creating and starting container #{@container_name} & running pbuilder"
135+
system("podman create -it #{create_opts}")
136+
exit(1) if $CHILD_STATUS.to_i.positive?
137+
system("podman start #{@container_name}")
138+
exit(1) if $CHILD_STATUS.to_i.positive?
139+
end
140+
141+
# Prevents clean-up after pbuilder finishes. There's no option
142+
# in pbuilder to do it, so we have to patch it manually. The issue is
143+
# with pbuilder not being able to delete some directories (presumably,
144+
# due to directory names starting with ".") and returning error.
145+
#
146+
# This little patch avoids the error by returning from the python cleanup
147+
# function early -- because the package itself is built successfully and
148+
# we don't actually care that pbuilder is unable to clean something up.
149+
# The container is going to be removed anyway, so it's even less work as
150+
# a result.
151+
def patch_pbuilder
152+
cmd = "sed -E -i \"s/(^function clean_subdirectories.*$)/\\1\\n return/g\" /usr/lib/pbuilder/pbuilder-modules"
153+
system("podman exec #{@container_name} /bin/bash -c '#{cmd}'")
154+
end
155+
156+
def run_build(pkg_type = :versioned)
157+
dsc_fn = "#{@spec.deb_pkg_name(pkg_type)}_0-1.dsc"
158+
deb_fn = "#{@spec.deb_pkg_name(pkg_type)}_0-1_#{@spec.arch}.deb"
159+
160+
cmds = []
161+
cmds << "dpkg-buildpackage --build=source -d" # -d flag helps with dependencies error
162+
cmds << "fakeroot pbuilder build ../#{dsc_fn}"
163+
cmds << "mv /var/cache/pbuilder/result/#{deb_fn} /root/pgpm/out/"
164+
165+
puts " Building package with pbuilder..."
166+
cmds.each do |cmd|
167+
system("podman exec -w /root/pgpm/source-#{pkg_type} #{@container_name} /bin/bash -c '#{cmd}'")
168+
exit(1) if $CHILD_STATUS.to_i.positive?
169+
end
170+
end
171+
172+
def copy_build_from_container(pkg_type = :versioned)
173+
puts "Copying .deb file from podman container into current directory..."
174+
deb_fn = "#{@spec.deb_pkg_name(pkg_type)}_0-1_#{@spec.arch}.deb"
175+
deb_copy_fn = "#{@spec.deb_pkg_name(pkg_type)}_#{@spec.arch}.deb"
176+
FileUtils.cp("#{@pgpm_dir}/out/#{deb_fn}", "#{Dir.pwd}/#{deb_copy_fn}")
177+
end
178+
179+
def cleanup
180+
puts "Cleaning up..."
181+
182+
puts " Stopping destroying podman container: #{@container_name}"
183+
system("podman container stop #{@container_name}")
184+
system("podman container rm #{@container_name}")
185+
186+
# Remove temporary files
187+
#
188+
# Make sure @pgpm_dir starts with "/tmp/" or we may accidentally
189+
# delete something everything! You can never be sure!
190+
if @pgpm_dir.start_with?("/tmp/")
191+
puts " Removing temporary files in #{@pgpm_dir}"
192+
FileUtils.rm_rf(@pgpm_dir)
193+
else
194+
puts "WARNING: will not remove temporary files, strange path: \"#{@pgpm_dir}\""
195+
end
196+
end
197+
198+
# Needed because SELinux requires :z suffix for mounted directories to
199+
# be accessible -- otherwise we get "Permission denied" when cd into a
200+
# mounted dir inside the container.
201+
def selinux_enabled?
202+
# This returns true or false by itself
203+
system("sestatus | grep 'SELinux status' | grep -o 'enabled'")
204+
end
205+
end
206+
end
207+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env bash
2+
apt update
3+
DEBIAN_FRONTEND=noninteractive apt -y install build-essential curl lsb-release ca-certificates
4+
5+
### PostgreSQL installation
6+
#
7+
install -d /usr/share/postgresql-common/pgdg
8+
curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
9+
10+
# Create the repository configuration file:
11+
sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
12+
13+
# Update the package lists:
14+
apt update
15+
16+
# Install the latest version of PostgreSQL:
17+
# If you want a specific version, use 'postgresql-16' or similar instead of 'postgresql'
18+
apt -y install postgresql-17 postgresql-server-dev-17 postgresql-common
19+
#
20+
### END OF PostgreSQL installation
21+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bash
2+
3+
ext_dir="$PGPM_INSTALL_ROOT/$(pg_config --sharedir)/extension"
4+
control_fn="$ext_dir/$PGPM_EXTENSION_NAME.control"
5+
6+
echo "Creating extension dir: $ext_dir"
7+
mkdir -p "$ext_dir"
8+
9+
echo "Creating control file: $control_fn"
10+
cp "$PGPM_BUILDROOT/$PGPM_EXTENSION_NAME.control" "$ext_dir/"
11+
echo >> "$control_fn"
12+
echo "default_version = '$PGPM_EXTENSION_VERSION'" >> "$control_fn"

lib/pgpm/deb/scripts/pg_config.sh

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#! /usr/bin/env bash
2+
3+
# Ensure PG_CONFIG is set
4+
if [[ -z "$PG_CONFIG" ]]; then
5+
echo "Error: PG_CONFIG is not set."
6+
exit 1
7+
fi
8+
9+
# Wrapper function for pg_config
10+
pg_config_wrapper() {
11+
"$PG_CONFIG" "$@" | while read -r line; do
12+
if [[ -n "$PGPM_REDIRECT_TO_BUILDROOT" && -f "$line" || -d "$line" ]]; then
13+
echo "$PGPM_INSTALL_ROOT$line"
14+
else
15+
echo "$line"
16+
fi
17+
done
18+
}
19+
20+
# Call the wrapper function with the arguments passed to the script
21+
pg_config_wrapper "$@"

0 commit comments

Comments
 (0)