pg-here 0.1.5 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,18 +33,18 @@ bunx pg-here
33
33
 
34
34
  This is the single command you want from any directory.
35
35
 
36
- If it still resolves an older release, the package `latest` dist-tag is behind:
36
+ If it still resolves an older release, the package `latest` dist-tag may be behind:
37
37
 
38
38
  To force that version and bypass a stale cache:
39
39
 
40
40
  ```bash
41
- bunx pg-here@0.1.5
41
+ bunx pg-here@0.1.8
42
42
  ```
43
43
 
44
44
  If maintainers want plain `bunx pg-here` to work for everyone, run once:
45
45
 
46
46
  ```bash
47
- npm dist-tag add pg-here@0.1.5 latest
47
+ npm dist-tag add pg-here@0.1.8 latest
48
48
  ```
49
49
 
50
50
  After that, this should work anywhere:
@@ -90,6 +90,37 @@ sudo apk add libxml2
90
90
 
91
91
  If that machine is intentionally minimal (or containerized), use an image with PostgreSQL runtime dependencies.
92
92
 
93
+ If apt says `Package 'libxml2' has no installation candidate`, your apt sources are likely missing standard repos.
94
+
95
+ ```bash
96
+ cat /etc/os-release
97
+ apt-cache search '^libxml2$'
98
+ ```
99
+
100
+ and if that returns nothing, fix apt sources for your distro/arch first, or use a base image that includes PostgreSQL runtime packages.
101
+
102
+ Important ABI note:
103
+
104
+ - If your host exposes `libxml2.so.16` but not `libxml2.so.2`, older releases may fail to start before applying fallback.
105
+ - If a failure mentions `/pg_local/bin/bin/postgres`, clear and redownload:
106
+
107
+ ```bash
108
+ rm -rf pg_local
109
+ bunx pg-here@0.1.8
110
+ ```
111
+
112
+ For this release, `0.1.8` also attempts a one-time compatibility workaround when it detects
113
+ `libxml2.so.16`-only hosts:
114
+ - it creates a local runtime symlink in `./pg_local/runtime-libs/libxml2.so.2` pointing to the discovered `libxml2.so.16`
115
+ - sets `LD_LIBRARY_PATH` for the retry so `postgres`/`initdb` can launch
116
+
117
+ If your system is locked down and this still fails, create a global compatibility symlink (where permitted):
118
+
119
+ ```bash
120
+ sudo ln -sfn /usr/lib/x86_64-linux-gnu/libxml2.so.16 /usr/local/lib/libxml2.so.2
121
+ sudo ldconfig
122
+ ```
123
+
93
124
  ### Programmatic usage
94
125
 
95
126
  Use `pg-here` directly from your server startup code and auto-create the app database if missing.
package/bin/pg-here.mjs CHANGED
@@ -3,7 +3,10 @@
3
3
  import yargs from "yargs";
4
4
  import { hideBin } from "yargs/helpers";
5
5
  import { startPgHere } from "../dist/index.js";
6
- import { maybePrintLinuxRuntimeHelp } from "../scripts/cli-error-help.mjs";
6
+ import {
7
+ maybePrintLinuxRuntimeHelp,
8
+ startPgHereWithLibxml2Compat,
9
+ } from "../scripts/cli-error-help.mjs";
7
10
 
8
11
  const argv = await yargs(hideBin(process.argv))
9
12
  .version(false)
@@ -33,8 +36,8 @@ const argv = await yargs(hideBin(process.argv))
33
36
  .parse();
34
37
 
35
38
  let pg;
36
- try {
37
- pg = await startPgHere({
39
+ const startInstance = () =>
40
+ startPgHere({
38
41
  projectDir: process.cwd(),
39
42
  port: argv.port,
40
43
  username: argv.username,
@@ -42,6 +45,9 @@ try {
42
45
  database: argv.database,
43
46
  postgresVersion: argv["pg-version"],
44
47
  });
48
+
49
+ try {
50
+ pg = await startPgHereWithLibxml2Compat(startInstance, process.cwd());
45
51
  } catch (error) {
46
52
  if (process.platform === "linux") {
47
53
  maybePrintLinuxRuntimeHelp(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pg-here",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
4
4
  "description": "Per-project embedded PostgreSQL with programmatic startup and auto DB creation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,11 +1,26 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { accessSync, existsSync, lstatSync, mkdirSync, readlinkSync, rmSync, symlinkSync, F_OK } from "node:fs";
3
+ import { join } from "node:path";
2
4
 
3
- const LIBXML_LIBRARY = /libxml2\.so\.2/;
5
+ const LIBXML2_SONAME = "libxml2.so.2";
6
+ const LIBXML2_ALTERNATE_SONAME = "libxml2.so.16";
7
+ const LIBXML2_COMPAT_DIR = "pg_local/runtime-libs";
8
+
9
+ const LIB_PATHS = [
10
+ "/usr/lib/x86_64-linux-gnu",
11
+ "/usr/lib/i386-linux-gnu",
12
+ "/usr/lib",
13
+ "/lib/x86_64-linux-gnu",
14
+ "/lib/i386-linux-gnu",
15
+ "/lib",
16
+ "/usr/local/lib",
17
+ ];
4
18
 
5
19
  export function maybePrintLinuxRuntimeHelp(error) {
6
20
  const message = String(error?.message ?? error);
7
21
  const missingLibsFromError = extractMissingLibrariesFromMessage(message);
8
22
  const missingLibsFromBinary = extractMissingLibrariesFromBinaryPath(message);
23
+ const binPath = extractBinaryPathFromError(message);
9
24
 
10
25
  const missingLibs = [...new Set([...missingLibsFromError, ...missingLibsFromBinary])];
11
26
 
@@ -18,20 +33,144 @@ export function maybePrintLinuxRuntimeHelp(error) {
18
33
  console.error(`Missing libraries: ${missingLibs.join(", ")}`);
19
34
  console.error();
20
35
  console.error("Install system packages for your distro and retry:");
21
- console.error(" Ubuntu/Debian: sudo apt-get update && sudo apt-get install -y libxml2");
22
- console.error(" Fedora/RHEL: sudo dnf install -y libxml2");
23
- console.error(" Alpine: sudo apk add libxml2");
24
36
  console.error(
25
- `If your distro requires different package names, install packages that provide: ${missingLibs.join(
26
- ", "
27
- )}`
37
+ " Ubuntu/Debian: sudo apt-get update && sudo apt-get install -y libxml2 libxml2-dev libxml2-utils"
38
+ );
39
+ console.error(
40
+ " Fedora/RHEL: sudo dnf install -y libxml2"
41
+ );
42
+ console.error(
43
+ " Alpine: sudo apk add libxml2"
44
+ );
45
+ console.error();
46
+ console.error(" Quick checks:");
47
+ console.error(
48
+ " find /usr/lib /usr/local/lib /lib -name 'libxml2.so.2*' 2>/dev/null | head"
28
49
  );
50
+ if (binPath) {
51
+ console.error(` ldd ${binPath} | grep libxml2`);
52
+ }
53
+ console.error(" sudo ldconfig && sudo ldconfig -p | grep libxml2.so.2");
54
+ if (binPath.includes("/bin/bin/postgres")) {
55
+ console.error(" Your local pg_local cache looks partially provisioned (bin/bin/postgres path).");
56
+ console.error(" Try removing it and retrying:");
57
+ console.error(" rm -rf pg_local && bunx pg-here@0.1.8");
58
+ }
59
+ console.error(
60
+ `If your distro requires different package names, install packages that provide: ${missingLibs.join(", ")}`
61
+ );
62
+ printPotentialSymlinkHint();
29
63
 
30
- if (LIBXML_LIBRARY.test(message)) {
31
- console.error("Hint: this project does not ship all optional shared objects in every Linux image.");
64
+ if (hasLibxml2CompatibilityNeed(error)) {
65
+ const fallback = findLibraryPath(LIBXML2_ALTERNATE_SONAME);
66
+ console.error();
67
+ console.error("Compatibility note:");
68
+ if (fallback) {
69
+ console.error(
70
+ `This host only provides ${LIBXML2_ALTERNATE_SONAME} at ${fallback}, not ${LIBXML2_SONAME}.`
71
+ );
72
+ console.error(
73
+ "This release now retries startup with a project-local symlink fallback automatically."
74
+ );
75
+ console.error(
76
+ `If that does not work, you can retry with a global symlink (requires sudo):\n sudo ln -sfn ${fallback} /usr/local/lib/${LIBXML2_SONAME} && sudo ldconfig`
77
+ );
78
+ } else {
79
+ console.error(
80
+ `Expected ${LIBXML2_SONAME} was not found and no ${LIBXML2_ALTERNATE_SONAME} fallback was discovered.`
81
+ );
82
+ console.error(
83
+ `Your host may be too minimal or custom; install a PostgreSQL-ready runtime stack for your distro.`
84
+ );
85
+ }
32
86
  }
33
87
  }
34
88
 
89
+ export function hasLibxml2CompatibilityNeed(error) {
90
+ const message = String(error?.message ?? error);
91
+ const missingFromMessage = extractMissingLibrariesFromMessage(message);
92
+ const missingFromBinary = extractMissingLibrariesFromBinaryPath(message);
93
+ const missing = [...missingFromMessage, ...missingFromBinary];
94
+ return missing.includes(LIBXML2_SONAME) || message.includes("libxml2.so.2");
95
+ }
96
+
97
+ export async function startPgHereWithLibxml2Compat(start, workingDir) {
98
+ try {
99
+ return await start();
100
+ } catch (error) {
101
+ if (!hasLibxml2CompatibilityNeed(error)) {
102
+ throw error;
103
+ }
104
+
105
+ const patched = ensureLibxml2Compatibility(workingDir);
106
+ if (!patched) {
107
+ throw error;
108
+ }
109
+
110
+ return await start();
111
+ }
112
+ }
113
+
114
+ export function ensureLibxml2Compatibility(workingDir) {
115
+ if (process.platform !== "linux") {
116
+ return false;
117
+ }
118
+
119
+ const projectDir = typeof workingDir === "string" && workingDir ? workingDir : process.cwd();
120
+ const compatDir = join(projectDir, LIBXML2_COMPAT_DIR);
121
+ const compatLib = join(compatDir, LIBXML2_SONAME);
122
+
123
+ if (findLibraryPath(LIBXML2_SONAME)) {
124
+ return false;
125
+ }
126
+
127
+ const fallback = findLibraryPath(LIBXML2_ALTERNATE_SONAME);
128
+ if (!fallback) {
129
+ return false;
130
+ }
131
+
132
+ try {
133
+ mkdirSync(compatDir, { recursive: true });
134
+ if (existsSync(compatLib)) {
135
+ const existing = readCompatLink(compatLib);
136
+ if (existing === fallback) {
137
+ ensureLdLibraryPath(compatDir);
138
+ return true;
139
+ }
140
+
141
+ rmSync(compatLib, { force: true });
142
+ }
143
+
144
+ symlinkSync(fallback, compatLib);
145
+ ensureLdLibraryPath(compatDir);
146
+ return true;
147
+ } catch {
148
+ return false;
149
+ }
150
+ }
151
+
152
+ function readCompatLink(path) {
153
+ try {
154
+ const info = lstatSync(path);
155
+ if (!info.isSymbolicLink()) {
156
+ return "";
157
+ }
158
+ return readlinkSync(path);
159
+ } catch {
160
+ return "";
161
+ }
162
+ }
163
+
164
+ function ensureLdLibraryPath(path) {
165
+ const currentPath = process.env.LD_LIBRARY_PATH ?? "";
166
+ const paths = currentPath.split(":").filter(Boolean);
167
+ if (paths.includes(path)) {
168
+ return;
169
+ }
170
+
171
+ process.env.LD_LIBRARY_PATH = path + (currentPath ? `:${currentPath}` : "");
172
+ }
173
+
35
174
  function extractMissingLibrariesFromMessage(message) {
36
175
  const regex = /([A-Za-z0-9._-]+\.so(?:\.\d+)*)\b/g;
37
176
  const matches = [...message.matchAll(regex)].map((match) => match[1]);
@@ -55,3 +194,68 @@ function extractMissingLibrariesFromBinaryPath(message) {
55
194
  (match) => match[1]
56
195
  );
57
196
  }
197
+
198
+ function extractBinaryPathFromError(message) {
199
+ const pathMatch = message.match(/(\/[^\s"]*\/bin\/postgres)/);
200
+ return pathMatch ? pathMatch[1] : "";
201
+ }
202
+
203
+ function hasLibxmlVersion16Only() {
204
+ const candidates = ["/usr/lib/x86_64-linux-gnu", "/usr/lib", "/lib/x86_64-linux-gnu", "/lib"];
205
+ for (const dir of candidates) {
206
+ try {
207
+ accessSync(`${dir}/libxml2.so.2`, F_OK);
208
+ return false;
209
+ } catch {
210
+ // continue
211
+ }
212
+
213
+ try {
214
+ accessSync(`${dir}/libxml2.so.16`, F_OK);
215
+ if (hasLibxmlVersion16AtPath(`${dir}/libxml2.so.16`)) {
216
+ return true;
217
+ }
218
+ } catch {}
219
+ }
220
+
221
+ return false;
222
+ }
223
+
224
+ function hasLibxmlVersion16AtPath(path) {
225
+ return typeof path === "string" && path.endsWith("/libxml2.so.16");
226
+ }
227
+
228
+ function printPotentialSymlinkHint() {
229
+ const result = spawnSync("sh", [
230
+ "-c",
231
+ "find /usr/lib /usr/local/lib /lib -name 'libxml2.so.2*' 2>/dev/null",
232
+ ], { encoding: "utf8" });
233
+
234
+ if (result.status !== 0) {
235
+ return;
236
+ }
237
+
238
+ const matches = (result.stdout ?? "").split("\n").filter(Boolean);
239
+ const exact = matches.find((item) => item.endsWith("/libxml2.so.2"));
240
+ if (!exact) {
241
+ console.error();
242
+ console.error("Hint:");
243
+ if (hasLibxmlVersion16Only()) {
244
+ console.error("This host appears to provide only libxml2.so.16. Compatibility is not guaranteed.");
245
+ } else {
246
+ console.error("No libxml2.so.2 file was found in the standard library paths.");
247
+ }
248
+ }
249
+ }
250
+
251
+ function findLibraryPath(name) {
252
+ const args = [...LIB_PATHS, "-name", `${name}*`, "-print"];
253
+ const result = spawnSync("find", args, { encoding: "utf8" });
254
+ const matches = (result.stdout ?? "").split("\n").filter(Boolean);
255
+ if (matches.length === 0) {
256
+ return "";
257
+ }
258
+
259
+ const exact = matches.find((item) => item.endsWith(`/${name}`));
260
+ return exact ?? matches[0];
261
+ }
@@ -1,7 +1,10 @@
1
1
  import yargs from "yargs";
2
2
  import { hideBin } from "yargs/helpers";
3
3
  import { startPgHere } from "../index.ts";
4
- import { maybePrintLinuxRuntimeHelp } from "./cli-error-help.mjs";
4
+ import {
5
+ maybePrintLinuxRuntimeHelp,
6
+ startPgHereWithLibxml2Compat,
7
+ } from "./cli-error-help.mjs";
5
8
 
6
9
  const argv = await yargs(hideBin(process.argv))
7
10
  .version(false)
@@ -31,8 +34,8 @@ const argv = await yargs(hideBin(process.argv))
31
34
  .parse();
32
35
 
33
36
  let pg;
34
- try {
35
- pg = await startPgHere({
37
+ const startInstance = () =>
38
+ startPgHere({
36
39
  projectDir: process.cwd(),
37
40
  port: argv.port,
38
41
  username: argv.username,
@@ -40,6 +43,9 @@ try {
40
43
  database: argv.database,
41
44
  postgresVersion: argv["pg-version"],
42
45
  });
46
+
47
+ try {
48
+ pg = await startPgHereWithLibxml2Compat(startInstance, process.cwd());
43
49
  } catch (error) {
44
50
  if (process.platform === "linux") {
45
51
  maybePrintLinuxRuntimeHelp(error);