Skip to content

Commit a0d3dff

Browse files
committed
fix(postgresql): place deb-extracted binaries at lib/postgresql/<major>/bin so PG can relocate sharedir
Closes #112. PostgreSQL's deb packages are compiled with the unusual split layout BINDIR=/usr/lib/postgresql/<major>/bin, SHAREDIR=/usr/share/postgresql/<major>. At runtime PG uses make_relative_path() (src/port/path.c) to relocate SHAREDIR (and PKGLIBDIR) relative to the running binary, but only if the binary's parent directory matches the bin_path tail after stripping the common prefix with target_path. For these compiled-in paths the common prefix is just "/usr/", so the binary's parent must end in "lib/postgresql/<major>/bin". dbdeployer previously placed the binaries at <basedir>/bin/, so the tail match failed and PG fell back to the absolute compiled-in path "/usr/share/postgresql/<major>" — which doesn't exist on a clean machine without postgresql-common installed. initdb then died with: FATAL: could not open directory "/usr/share/postgresql/16/timezonesets" The existing code already double-copied share into both <basedir>/share/ (for `initdb -L`) and <basedir>/share/postgresql/<major>/ (for runtime relocation) — half the puzzle. The missing half was the binary placement. Changes in providers/postgresql/unpack.go: - Real binaries go to <basedir>/lib/postgresql/<major>/bin/ - Extension libs (PKGLIBDIR) go to <basedir>/lib/postgresql/<major>/lib/ - <basedir>/bin/<binary> are now relative symlinks into the nested bin/. On Linux find_my_exec() reads /proc/self/exe which resolves symlinks to the real file, so launching via the symlink still gives PG the relocatable path it needs. Existing callers (sandbox.go, scripts.go) reach binaries via <basedir>/bin/ as before — no API change. - UnpackDebs is now idempotent: it removes any prior bin/, lib/, share/ artifacts before recreating them, so users can re-run unpack to migrate from the old (broken) layout to the new one without a separate flag. Changes in providers/postgresql/sandbox.go: - New checkLinuxLayout() runs at the top of CreateSandbox. If it finds <basedir>/bin/postgres is a regular file (rather than the new symlink) on Linux, it returns an error pointing the user at `dbdeployer unpack` to fix the layout. This replaces the cryptic "/usr/share/postgresql/16/timezonesets" error with an actionable one. - macOS path is unchanged. Postgres.app's PG is compiled with the standard --prefix layout where BINDIR=<prefix>/bin and SHAREDIR=<prefix>/share/postgresql, so the common prefix covers the whole <prefix>/ and the bin tail is just "bin" — which dbdeployer's <basedir>/bin/ already satisfies. Manually verified end-to-end on a clean Ubuntu 24.04-like environment with `apt-get download postgresql-16 postgresql-client-16 && dbdeployer unpack --provider=postgresql ... && dbdeployer deploy postgresql 16.13`. Initdb succeeds, sandbox starts, SELECT version() returns the expected build, clean shutdown. Old-layout detection also verified by copying a regular file over the symlink and confirming the deploy fails with the new actionable message. The CI integration test currently masks this bug by installing postgresql-16 system-wide before unpacking — a follow-up PR will switch CI to `apt-get download` only.
1 parent 857149f commit a0d3dff

2 files changed

Lines changed: 86 additions & 18 deletions

File tree

providers/postgresql/sandbox.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"os/exec"
77
"path/filepath"
8+
"runtime"
89

910
"github.com/ProxySQL/dbdeployer/providers"
1011
)
@@ -20,6 +21,10 @@ func (p *PostgreSQLProvider) CreateSandbox(config providers.SandboxConfig) (*pro
2021
logDir := filepath.Join(dataDir, "log")
2122
logFile := filepath.Join(config.Dir, "postgresql.log")
2223

24+
if err := checkLinuxLayout(basedir, binDir); err != nil {
25+
return nil, err
26+
}
27+
2328
replication := config.Options["replication"] == "true"
2429

2530
// Run initdb (data dir must not exist or must be empty)
@@ -94,3 +99,35 @@ func (p *PostgreSQLProvider) resolveBasedir(config providers.SandboxConfig) (str
9499
}
95100
return basedirFromVersion(config.Version)
96101
}
102+
103+
// checkLinuxLayout detects PostgreSQL extractions produced by older
104+
// versions of dbdeployer (before issue #112 was fixed). In the old layout,
105+
// <basedir>/bin/<binary> were regular files; PG's make_relative_path() then
106+
// failed to relocate the compiled-in SHAREDIR (`/usr/share/postgresql/<major>`),
107+
// so initdb died with "could not open directory /usr/share/postgresql/<major>/timezonesets".
108+
//
109+
// New extractions place the real binaries under
110+
// <basedir>/lib/postgresql/<major>/bin/ and expose them via symlinks at
111+
// <basedir>/bin/. If we find a regular file there on Linux, the user
112+
// needs to re-run unpack.
113+
func checkLinuxLayout(basedir, binDir string) error {
114+
if runtime.GOOS != "linux" {
115+
return nil
116+
}
117+
fi, err := os.Lstat(filepath.Join(binDir, "postgres"))
118+
if err != nil {
119+
// Missing binary will be reported by the initdb step below with a
120+
// clearer error than we could produce here.
121+
return nil
122+
}
123+
if fi.Mode()&os.ModeSymlink != 0 {
124+
return nil
125+
}
126+
return fmt.Errorf(
127+
"PostgreSQL binaries at %s were unpacked by an older dbdeployer using\n"+
128+
"a layout incompatible with deb-packaged PostgreSQL — initdb would fail\n"+
129+
"to find share files (see issue #112). Re-run unpack to fix:\n"+
130+
" dbdeployer unpack --provider=postgresql <server.deb> <client.deb>",
131+
basedir,
132+
)
133+
}

providers/postgresql/unpack.go

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,50 @@ func UnpackDebs(serverDeb, clientDeb, targetDir string) error {
6363
major := strings.Split(version, ".")[0]
6464

6565
srcBin := filepath.Join(tmpDir, "usr", "lib", "postgresql", major, "bin")
66-
srcLib := filepath.Join(tmpDir, "usr", "lib", "postgresql", major, "lib")
66+
srcPgLib := filepath.Join(tmpDir, "usr", "lib", "postgresql", major, "lib")
6767
srcShare := filepath.Join(tmpDir, "usr", "share", "postgresql", major)
6868

69-
dstBin := filepath.Join(targetDir, "bin")
70-
dstLib := filepath.Join(targetDir, "lib")
71-
dstShare := filepath.Join(targetDir, "share")
69+
// Destination layout mirrors Debian's compile-time prefix so that PG's
70+
// make_relative_path() (src/port/path.c) succeeds at runtime. Debian
71+
// builds PG with BINDIR=/usr/lib/postgresql/<major>/bin and
72+
// SHAREDIR=/usr/share/postgresql/<major>; the common prefix is just
73+
// /usr/, so for PG to relocate SHAREDIR to <basedir>/share/postgresql/<major>
74+
// the running binary's parent directory must end in
75+
// "lib/postgresql/<major>/bin". Same logic applies to PKGLIBDIR.
76+
//
77+
// We therefore place the real binaries and extension libs under
78+
// <basedir>/lib/postgresql/<major>/, and expose them via symlinks at
79+
// <basedir>/bin/ so existing callers (sandbox.go, scripts.go) keep
80+
// working. On Linux, PG's find_my_exec() reads /proc/self/exe which
81+
// resolves symlinks to the real file, so launching via the symlink
82+
// still gives PG the relocatable path it needs.
83+
dstPgBin := filepath.Join(targetDir, "lib", "postgresql", major, "bin")
84+
dstPgLib := filepath.Join(targetDir, "lib", "postgresql", major, "lib")
85+
dstShare := filepath.Join(targetDir, "share") // flat, for `initdb -L`
86+
dstPgShare := filepath.Join(targetDir, "share", "postgresql", major) // nested, for postgres runtime
87+
dstBin := filepath.Join(targetDir, "bin") // symlinks into dstPgBin
7288

73-
for _, dir := range []string{dstBin, dstLib, dstShare} {
89+
// Make UnpackDebs idempotent: remove any prior extraction artifacts
90+
// in the subtrees we own so re-running unpack picks up the latest
91+
// layout cleanly (in particular when migrating from a pre-fix layout
92+
// where <basedir>/bin/<binary> were real files instead of symlinks).
93+
for _, dir := range []string{dstBin, filepath.Join(targetDir, "lib"), dstShare} {
94+
if err := os.RemoveAll(dir); err != nil {
95+
return fmt.Errorf("removing stale %s: %w", dir, err)
96+
}
97+
}
98+
99+
for _, dir := range []string{dstPgBin, dstPgLib, dstShare, dstPgShare, dstBin} {
74100
if err := os.MkdirAll(dir, 0755); err != nil {
75101
return fmt.Errorf("creating directory %s: %w", dir, err)
76102
}
77103
}
78104

79105
copies := []struct{ src, dst string }{
80-
{srcBin, dstBin},
81-
{srcLib, dstLib},
82-
{srcShare, dstShare},
106+
{srcBin, dstPgBin},
107+
{srcPgLib, dstPgLib},
108+
{srcShare, dstShare}, // flat copy for `initdb -L`
109+
{srcShare, dstPgShare}, // nested copy for postgres server runtime
83110
}
84111
for _, c := range copies {
85112
if _, err := os.Stat(c.src); os.IsNotExist(err) {
@@ -91,17 +118,21 @@ func UnpackDebs(serverDeb, clientDeb, targetDir string) error {
91118
}
92119
}
93120

94-
// The postgres binary resolves share data relative to its own binary as
95-
// ../share/postgresql/<major>/ (compiled-in prefix from deb packaging).
96-
// Copy share files there too so both initdb (-L share/) and the postgres
97-
// server binary can find timezonesets and other share data.
98-
pgShareCompat := filepath.Join(dstShare, "postgresql", major)
99-
if err := os.MkdirAll(pgShareCompat, 0755); err != nil {
100-
return fmt.Errorf("creating compat share dir: %w", err)
121+
// Expose every server-side binary as <basedir>/bin/<name>, symlinked
122+
// into the nested lib/postgresql/<major>/bin/ directory.
123+
entries, err := os.ReadDir(dstPgBin)
124+
if err != nil {
125+
return fmt.Errorf("reading %s: %w", dstPgBin, err)
101126
}
102-
compatCmd := exec.Command("cp", "-a", srcShare+"/.", pgShareCompat+"/") //nolint:gosec // paths are from controlled deb extraction
103-
if output, err := compatCmd.CombinedOutput(); err != nil {
104-
return fmt.Errorf("copying share to compat path: %s: %w", string(output), err)
127+
for _, entry := range entries {
128+
if entry.IsDir() {
129+
continue
130+
}
131+
linkTarget := filepath.Join("..", "lib", "postgresql", major, "bin", entry.Name())
132+
linkPath := filepath.Join(dstBin, entry.Name())
133+
if err := os.Symlink(linkTarget, linkPath); err != nil {
134+
return fmt.Errorf("symlinking %s -> %s: %w", linkPath, linkTarget, err)
135+
}
105136
}
106137

107138
for _, bin := range RequiredBinaries() {

0 commit comments

Comments
 (0)