From 461e92d67e6d4e01eb147f4fbf83171c8c0efe15 Mon Sep 17 00:00:00 2001 From: tf Date: Fri, 19 Jun 2026 15:22:24 +0200 Subject: [PATCH 1/6] docs: add Windows build and run guide --- .gitignore | 1 - docs/WINDOWS_BUILD.md | 189 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 docs/WINDOWS_BUILD.md diff --git a/.gitignore b/.gitignore index 12bbacf..2efcdd4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ .env .claude -/docs \ No newline at end of file diff --git a/docs/WINDOWS_BUILD.md b/docs/WINDOWS_BUILD.md new file mode 100644 index 0000000..edf701f --- /dev/null +++ b/docs/WINDOWS_BUILD.md @@ -0,0 +1,189 @@ +# Building and running the agent on Windows + +This guide covers building `portabase-agent` from source on Windows and +running it locally, including the workarounds needed for two +Windows-specific issues: + +1. `openssl-sys` fails to build (native TLS dependency). +2. `select_pg_path` needs a PostgreSQL install that Windows doesn't ship + at the Debian/Ubuntu path the code originally assumed (see PR + "fix(postgres): make `select_pg_path` cross-platform"). + +## Prerequisites + +- **Rust** (stable toolchain, MSVC target — the default on Windows): + `rustup default stable-x86_64-pc-windows-msvc` +- **Visual Studio Build Tools 2022**, with the "Desktop development with + C++" workload (provides `cl.exe`/`nmake.exe`, required to compile any + native dependency). +- **PostgreSQL** for Windows (the official EDB installer), e.g. version + 16: https://www.postgresql.org/download/windows/ + Default install path: `C:\Program Files\PostgreSQL\16`. +- **Redis** for Windows. There is no official Windows build of Redis; + this guide uses the community-maintained + [redis-windows](https://github.com/redis-windows/redis-windows) + distribution. +- **vcpkg**, to provide a prebuilt OpenSSL (see below — the `vendored` + OpenSSL feature currently fails to build on Windows for this project). + +## 1. OpenSSL via vcpkg + +The `vendored` OpenSSL feature (building OpenSSL from source via +`openssl-sys`) is the usual cross-platform fallback, but it did not +build successfully in testing on Windows (`nmake`-related hang/build +failure). Using a prebuilt OpenSSL via **vcpkg** worked reliably +instead: + +```powershell +git clone https://github.com/microsoft/vcpkg +.\vcpkg\bootstrap-vcpkg.bat +.\vcpkg\vcpkg install openssl:x64-windows +``` + +Then point the build at it: + +```powershell +$env:OPENSSL_DIR = "C:\path\to\vcpkg\installed\x64-windows" +``` + +Set this permanently (System Properties → Environment Variables) if you +don't want to re-export it in every new shell. + +> If you do want to retry the `vendored` route instead, you'll +> additionally need Perl (Strawberry Perl) and NASM on `PATH`. Not +> required when using the vcpkg approach above. + +## 2. Build + +```powershell +git clone https://github.com/Portabase/agent.git +cd agent +cargo build --release +``` + +`cargo build` (debug) also works for local testing; the commands below +assume a debug build (`target\debug\`) to match local development, swap +in `target\release\` for a release build. + +### Binary name + +The build currently produces `app.exe` (taken from the crate/package +name in `Cargo.toml`). For a clearer, branded executable, rename the +binary output to `portabase-agent.exe` by setting an explicit binary +name in `Cargo.toml`: + +```toml +[[bin]] +name = "portabase-agent" +path = "src/main.rs" +``` + +After this change, the build output becomes +`target\debug\portabase-agent.exe` (or `target\release\...` for release +builds). The run instructions below use `portabase-agent.exe`; replace +with `app.exe` if you haven't made this change. + +## 3. Run Redis + +Download/clone [redis-windows](https://github.com/redis-windows/redis-windows) +and start it on a custom port (here `65515`, to avoid clashing with any +other local Redis instance on the default `6379`): + +```powershell +redis-server.exe --port 65515 +``` + +Keep this running in its own terminal window. + +## 4. Configure environment variables + +The agent reads its configuration from environment variables. Example +startup script (adjust paths/values for your machine): + +```bat +set "TZ=Europe/Berlin" +set "POLLING=60" +set "APP_ENV=production" +set "DATA_PATH=D:\pg_tools\agent\target\debug\data" +rem set "DATABASES_CONFIG_FILE" +set "CELERY_BROKER_URL=redis://localhost:65515/" +call "C:\Program Files\PostgreSQL\16\pg_env.bat" +portabase-agent.exe +``` + +Notes: + +- `pg_env.bat` (shipped with the PostgreSQL installer) sets up + `PATH`/`PGBIN` and other Postgres environment variables for the + current shell — convenient as an alternative or complement to setting + `PG_BIN_DIR` manually (see the `select_pg_path` cross-platform fix). +- `DATA_PATH` should point to a writable directory; create it beforehand + if it doesn't exist yet (`mkdir D:\pg_tools\agent\target\debug\data`). +- Save the block above as e.g. `run-agent.bat` next to the executable + for repeatable local runs. + +### Using a `.env` file instead + +Manually `set`-ing variables in a batch file works, but is easy to lose +track of and doesn't play well with version control hygiene (secrets +end up in shell history). If the agent does not yet support loading a +`.env` file, consider adding support via the [`dotenvy`](https://docs.rs/dotenvy) +crate (maintained successor of `dotenv`) early in `main()`: + +```rust +fn main() { + // Loads variables from a `.env` file in the current directory, if + // present. Existing environment variables are not overridden, so + // this is safe to call even when variables are already set + // externally (e.g. by a process manager or CI). + let _ = dotenvy::dotenv(); + + // ... existing startup code +} +``` + +```toml +[dependencies] +dotenvy = "0.15" +``` + +Example `.env` file (place next to the executable, do **not** commit +this file — add `.env` to `.gitignore`): + +```env +TZ=Europe/Berlin +POLLING=60 +APP_ENV=production +DATA_PATH=D:\pg_tools\agent\target\debug\data +CELERY_BROKER_URL=redis://localhost:65515/ +``` + +Note that `.env` files are a convenient alternative to the `set` +commands above, but won't run `pg_env.bat` for you — either keep that +`call` in a small wrapper script, or set `PG_BIN_DIR` directly in the +`.env` file once the cross-platform `select_pg_path` fix is in place, +e.g.: + +```env +PG_BIN_DIR=C:\Program Files\PostgreSQL\16\bin +``` + +## 5. Start the agent + +With Redis running and the environment configured (either via the batch +script or a `.env` file): + +```powershell +.\portabase-agent.exe +``` + +## Troubleshooting + +- **`openssl-sys` build fails / hangs on `nmake`**: use the vcpkg + approach above instead of the `vendored` feature. +- **`pg_dump`/`pg_restore` not found**: make sure either `pg_env.bat` + was called in the current shell, `PG_BIN_DIR` is set, or PostgreSQL + is installed at the default `C:\Program Files\PostgreSQL\\bin` + location. +- **Redis connection refused**: confirm `redis-server.exe` is running + and that `CELERY_BROKER_URL` matches the port it's listening on. From 2052ab0ff50743a6fd118526c1eaa6125ee6e636 Mon Sep 17 00:00:00 2001 From: tf Date: Fri, 19 Jun 2026 15:27:45 +0200 Subject: [PATCH 2/6] fix(postgres): resolve pg_dump/pg_restore path cross-platform --- src/domain/postgres/connection.rs | 148 ++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/src/domain/postgres/connection.rs b/src/domain/postgres/connection.rs index 358b354..ae6c87f 100644 --- a/src/domain/postgres/connection.rs +++ b/src/domain/postgres/connection.rs @@ -32,11 +32,80 @@ pub async fn server_version(cfg: &DatabaseConfig) -> Result { Ok(version) } +/// Resolves the `bin` directory of a PostgreSQL installation for the given +/// major version, in a cross-platform way. +/// +/// Resolution order: +/// 1. The `PG_BIN_DIR` environment variable, if set, is used as-is. This +/// allows users/CI to override detection for non-standard installs +/// (e.g. portable PostgreSQL distributions, custom install locations). +/// 2. Platform-specific default install locations (Debian/Ubuntu packages, +/// the official Windows installer, Homebrew/Postgres.app on macOS, and +/// common RPM-based layouts on other Linux distros). +/// 3. A `PATH` lookup for `pg_dump` (`pg_dump.exe` on Windows), returning +/// its parent directory. +/// 4. The historical Debian/Ubuntu path as a last-resort fallback, so the +/// function keeps returning a `PathBuf` (never panics) even when nothing +/// was found, preserving the previous behavior for callers. pub fn select_pg_path(version: &str) -> std::path::PathBuf { let major = version.split('.').next().unwrap_or("17"); + + if let Ok(dir) = std::env::var("PG_BIN_DIR") { + return dir.into(); + } + + let candidates: Vec = if cfg!(target_os = "windows") { + vec![ + // Default install path used by the official EDB Windows installer + format!(r"C:\Program Files\PostgreSQL\{major}\bin").into(), + format!(r"C:\Program Files (x86)\PostgreSQL\{major}\bin").into(), + ] + } else if cfg!(target_os = "macos") { + vec![ + // Homebrew on Apple Silicon + format!("/opt/homebrew/opt/postgresql@{major}/bin").into(), + // Homebrew on Intel + format!("/usr/local/opt/postgresql@{major}/bin").into(), + // Postgres.app + format!("/Applications/Postgres.app/Contents/Versions/{major}/bin").into(), + ] + } else { + vec![ + // Debian/Ubuntu packages + format!("/usr/lib/postgresql/{major}/bin").into(), + // Common RPM-based distro layout + format!("/usr/pgsql-{major}/bin").into(), + ] + }; + + if let Some(found) = candidates.into_iter().find(|p| pg_dump_exists_in(p)) { + return found; + } + + if let Some(dir) = find_pg_dump_in_path() { + return dir; + } + format!("/usr/lib/postgresql/{}/bin", major).into() } +fn pg_dump_binary_name() -> &'static str { + if cfg!(target_os = "windows") { + "pg_dump.exe" + } else { + "pg_dump" + } +} + +fn pg_dump_exists_in(dir: &std::path::Path) -> bool { + dir.join(pg_dump_binary_name()).is_file() +} + +fn find_pg_dump_in_path() -> Option { + let path_var = std::env::var_os("PATH")?; + std::env::split_paths(&path_var).find(|dir| pg_dump_exists_in(dir)) +} + pub async fn terminate_connections(cfg: &DatabaseConfig) -> Result<()> { let mut admin = cfg.clone(); admin.database = "postgres".to_string().into(); @@ -67,11 +136,90 @@ pub fn detect_format_from_file(restore_file: &Path) -> PostgresDumpFormat { } } +#[cfg(test)] +mod select_pg_path_tests { + use super::*; + use std::sync::Mutex; + + // `std::env::set_var`/`remove_var` are process-global and, as of the + // 2024 edition, marked `unsafe` because mutating them concurrently + // from multiple threads is undefined behavior. Rust runs tests in + // parallel by default, so without serializing access here, these + // tests could race against each other over `PG_BIN_DIR`. + static ENV_LOCK: Mutex<()> = Mutex::new(()); + + // Tests below intentionally avoid asserting on whether a *real* + // PostgreSQL install is or isn't found on the machine running the + // tests (CI runners and developer machines may or may not have one, + // at any version) — that would make the tests environment-dependent + // and flaky. Instead, `PG_BIN_DIR` is always set to a deterministic + // value so behavior doesn't depend on the local system. + + #[test] + fn respects_pg_bin_dir_override() { + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let custom = if cfg!(target_os = "windows") { + r"C:\custom\pg\bin" + } else { + "/custom/pg/bin" + }; + // SAFETY: serialized via ENV_LOCK. + unsafe { + std::env::set_var("PG_BIN_DIR", custom); + } + let path = select_pg_path("16.4"); + // SAFETY: serialized via ENV_LOCK. + unsafe { + std::env::remove_var("PG_BIN_DIR"); + } + assert_eq!(path, std::path::PathBuf::from(custom)); + } + + #[test] + fn pg_bin_dir_override_ignores_requested_version() { + // The override is taken as-is, regardless of which version was + // requested — this documents/locks in that behavior. + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let custom = if cfg!(target_os = "windows") { + r"C:\custom\pg\bin" + } else { + "/custom/pg/bin" + }; + // SAFETY: serialized via ENV_LOCK. + unsafe { + std::env::set_var("PG_BIN_DIR", custom); + } + let path = select_pg_path("not-a-version"); + // SAFETY: serialized via ENV_LOCK. + unsafe { + std::env::remove_var("PG_BIN_DIR"); + } + assert_eq!(path, std::path::PathBuf::from(custom)); + } + + #[test] + fn pg_dump_binary_name_is_platform_specific() { + let name = pg_dump_binary_name(); + if cfg!(target_os = "windows") { + assert_eq!(name, "pg_dump.exe"); + } else { + assert_eq!(name, "pg_dump"); + } + } + + #[test] + fn pg_dump_exists_in_is_false_for_nonexistent_dir() { + let dir = std::path::Path::new("this/path/almost-certainly/does-not-exist-12345"); + assert!(!pg_dump_exists_in(dir)); + } +} + pub async fn detect_format_from_size(cfg: &DatabaseConfig) -> PostgresDumpFormat { info!( "Detecting database format {:?} - {:?}", cfg.name, cfg.generated_id ); + let client = match connect(cfg).await { Ok(c) => c, Err(_) => return PostgresDumpFormat::Fc, From befec0deae7ef5f65a46e7ec4cff415c24d4be50 Mon Sep 17 00:00:00 2001 From: tf Date: Fri, 19 Jun 2026 16:01:45 +0200 Subject: [PATCH 3/6] Windows Build workflow --- .github/workflows/windows-release.yml | 98 +++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .github/workflows/windows-release.yml diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml new file mode 100644 index 0000000..3e51204 --- /dev/null +++ b/.github/workflows/windows-release.yml @@ -0,0 +1,98 @@ +name: Build Windows release + +on: + workflow_dispatch: + push: + tags: + - 'v*' + branches: + - main + - master + +jobs: + build-windows: + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Rust toolchain (MSVC) + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-pc-windows-msvc + profile: minimal + override: true + + - name: Install vcpkg and OpenSSL (x64) + shell: pwsh + run: | + # Install vcpkg and the prebuilt OpenSSL package + git clone https://github.com/microsoft/vcpkg C:\vcpkg + C:\vcpkg\bootstrap-vcpkg.bat + C:\vcpkg\vcpkg install openssl:x64-windows + # Export variables for subsequent steps + 'VCPKG_ROOT=C:\vcpkg' | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + 'OPENSSL_DIR=C:\vcpkg\installed\x64-windows' | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Build (cargo release) + shell: pwsh + env: + # Cargo / openssl-sys will pick up OPENSSL_DIR from the environment + OPENSSL_DIR: ${{ env.OPENSSL_DIR }} + run: | + # Ensure the environment variable is present for this step + if (-Not $env:OPENSSL_DIR) { Write-Host "OPENSSL_DIR not set, printing env for debugging"; Get-ChildItem Env: | ForEach-Object { Write-Host $_ } } + cargo build --release + + - name: Prepare artifact zip + id: prepare_artifact + shell: pwsh + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + $tag = $env:RELEASE_TAG + if (-not $tag) { $tag = $env:GITHUB_SHA } + + # Find the built executable. Prefer an explicit portabase-agent.exe, fall back to first exe in target/release + $exe = "target\release\portabase-agent.exe" + if (-not (Test-Path $exe)) { + $first = Get-ChildItem -Path target\release -Filter *.exe | Select-Object -First 1 + if ($first) { $exe = $first.FullName } + } + + if (-not (Test-Path $exe)) { Write-Error "Built binary not found in target/release"; exit 1 } + + $outDir = "artifact" + New-Item -ItemType Directory -Path $outDir -Force | Out-Null + Copy-Item -Path $exe -Destination $outDir\ + + $zipName = "windows-release-$tag.zip" + if (Test-Path $zipName) { Remove-Item $zipName } + Compress-Archive -Path "$outDir\*" -DestinationPath $zipName -Force + Write-Host "ZIP=$zipName" + Write-Output "zip=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: windows-release + path: windows-release-*.zip + + - name: Create GitHub Release + if: startsWith(github.ref, 'refs/tags/') + id: create_release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.ref_name }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload release asset + if: startsWith(github.ref, 'refs/tags/') + uses: actions/upload-release-asset@v1 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: windows-release-${{ github.ref_name }}.zip + asset_name: windows-release-${{ github.ref_name }}.zip + asset_content_type: application/zip \ No newline at end of file From 0f6c93ecd04458f9cbef3749868198b204f1c473 Mon Sep 17 00:00:00 2001 From: tf Date: Fri, 19 Jun 2026 16:31:21 +0200 Subject: [PATCH 4/6] fix encoding --- src/domain/postgres/connection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/postgres/connection.rs b/src/domain/postgres/connection.rs index ae6c87f..16c21bd 100644 --- a/src/domain/postgres/connection.rs +++ b/src/domain/postgres/connection.rs @@ -151,7 +151,7 @@ mod select_pg_path_tests { // Tests below intentionally avoid asserting on whether a *real* // PostgreSQL install is or isn't found on the machine running the // tests (CI runners and developer machines may or may not have one, - // at any version) — that would make the tests environment-dependent + // at any version) — that would make the tests environment-dependent // and flaky. Instead, `PG_BIN_DIR` is always set to a deterministic // value so behavior doesn't depend on the local system. @@ -178,7 +178,7 @@ mod select_pg_path_tests { #[test] fn pg_bin_dir_override_ignores_requested_version() { // The override is taken as-is, regardless of which version was - // requested — this documents/locks in that behavior. + // requested — this documents/locks in that behavior. let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let custom = if cfg!(target_os = "windows") { r"C:\custom\pg\bin" From 16152328b0d19343096a2c8cbd92e56158519bd1 Mon Sep 17 00:00:00 2001 From: Charles GTE Date: Sat, 20 Jun 2026 12:31:01 +0200 Subject: [PATCH 5/6] fix: extract the env variable to the settings.rs and some refactoring, and the trigger in windows-release.yml --- .github/workflows/windows-release.yml | 3 +- .gitignore | 2 + docker-compose.yml | 2 +- docs/WINDOWS_BUILD.md | 189 -------------------------- src/domain/postgres/connection.rs | 98 +++---------- src/domain/postgres/mod.rs | 2 +- src/settings.rs | 2 + src/tests/domain/postgres.rs | 61 +++++++++ 8 files changed, 85 insertions(+), 274 deletions(-) delete mode 100644 docs/WINDOWS_BUILD.md diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index 3e51204..5f566e9 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -1,10 +1,11 @@ name: Build Windows release + on: workflow_dispatch: push: tags: - - 'v*' + - '[0-9]+.[0-9]+.[0-9]+' branches: - main - master diff --git a/.gitignore b/.gitignore index 2efcdd4..4e7fdb7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ .env .claude + +/docs diff --git a/docker-compose.yml b/docker-compose.yml index 2ad103b..37fb676 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: APP_ENV: development LOG: debug TZ: "Europe/Paris" - EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiNmNiMmQwODMtZDQ4MS00MWY3LTk5NjItZjNhMzU2ZTJiMzllIiwibWFzdGVyS2V5QjY0IjoiMUh0djdtWCtYVkJxL0IzUEV2WDlZZjlQeUdVZW5oRHlXemo5THRqNW90WT0ifQ==" + EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiNWRkZTE1NTctZWQ1ZC00MjUxLThiZDMtMDE0MjkxOTg2OGZjIiwibWFzdGVyS2V5QjY0IjoiQlhWM1hvbEM2NTZTVjdkTmdjV1BHUWxrKytycExJNmxHRGk3Q1BCNWllbz0ifQ==" #CHUNK_SIZE_MB: "1" #POOLING: 1 #DATABASES_CONFIG_FILE: "config.toml" diff --git a/docs/WINDOWS_BUILD.md b/docs/WINDOWS_BUILD.md deleted file mode 100644 index edf701f..0000000 --- a/docs/WINDOWS_BUILD.md +++ /dev/null @@ -1,189 +0,0 @@ -# Building and running the agent on Windows - -This guide covers building `portabase-agent` from source on Windows and -running it locally, including the workarounds needed for two -Windows-specific issues: - -1. `openssl-sys` fails to build (native TLS dependency). -2. `select_pg_path` needs a PostgreSQL install that Windows doesn't ship - at the Debian/Ubuntu path the code originally assumed (see PR - "fix(postgres): make `select_pg_path` cross-platform"). - -## Prerequisites - -- **Rust** (stable toolchain, MSVC target — the default on Windows): - `rustup default stable-x86_64-pc-windows-msvc` -- **Visual Studio Build Tools 2022**, with the "Desktop development with - C++" workload (provides `cl.exe`/`nmake.exe`, required to compile any - native dependency). -- **PostgreSQL** for Windows (the official EDB installer), e.g. version - 16: https://www.postgresql.org/download/windows/ - Default install path: `C:\Program Files\PostgreSQL\16`. -- **Redis** for Windows. There is no official Windows build of Redis; - this guide uses the community-maintained - [redis-windows](https://github.com/redis-windows/redis-windows) - distribution. -- **vcpkg**, to provide a prebuilt OpenSSL (see below — the `vendored` - OpenSSL feature currently fails to build on Windows for this project). - -## 1. OpenSSL via vcpkg - -The `vendored` OpenSSL feature (building OpenSSL from source via -`openssl-sys`) is the usual cross-platform fallback, but it did not -build successfully in testing on Windows (`nmake`-related hang/build -failure). Using a prebuilt OpenSSL via **vcpkg** worked reliably -instead: - -```powershell -git clone https://github.com/microsoft/vcpkg -.\vcpkg\bootstrap-vcpkg.bat -.\vcpkg\vcpkg install openssl:x64-windows -``` - -Then point the build at it: - -```powershell -$env:OPENSSL_DIR = "C:\path\to\vcpkg\installed\x64-windows" -``` - -Set this permanently (System Properties → Environment Variables) if you -don't want to re-export it in every new shell. - -> If you do want to retry the `vendored` route instead, you'll -> additionally need Perl (Strawberry Perl) and NASM on `PATH`. Not -> required when using the vcpkg approach above. - -## 2. Build - -```powershell -git clone https://github.com/Portabase/agent.git -cd agent -cargo build --release -``` - -`cargo build` (debug) also works for local testing; the commands below -assume a debug build (`target\debug\`) to match local development, swap -in `target\release\` for a release build. - -### Binary name - -The build currently produces `app.exe` (taken from the crate/package -name in `Cargo.toml`). For a clearer, branded executable, rename the -binary output to `portabase-agent.exe` by setting an explicit binary -name in `Cargo.toml`: - -```toml -[[bin]] -name = "portabase-agent" -path = "src/main.rs" -``` - -After this change, the build output becomes -`target\debug\portabase-agent.exe` (or `target\release\...` for release -builds). The run instructions below use `portabase-agent.exe`; replace -with `app.exe` if you haven't made this change. - -## 3. Run Redis - -Download/clone [redis-windows](https://github.com/redis-windows/redis-windows) -and start it on a custom port (here `65515`, to avoid clashing with any -other local Redis instance on the default `6379`): - -```powershell -redis-server.exe --port 65515 -``` - -Keep this running in its own terminal window. - -## 4. Configure environment variables - -The agent reads its configuration from environment variables. Example -startup script (adjust paths/values for your machine): - -```bat -set "TZ=Europe/Berlin" -set "POLLING=60" -set "APP_ENV=production" -set "DATA_PATH=D:\pg_tools\agent\target\debug\data" -rem set "DATABASES_CONFIG_FILE" -set "CELERY_BROKER_URL=redis://localhost:65515/" -call "C:\Program Files\PostgreSQL\16\pg_env.bat" -portabase-agent.exe -``` - -Notes: - -- `pg_env.bat` (shipped with the PostgreSQL installer) sets up - `PATH`/`PGBIN` and other Postgres environment variables for the - current shell — convenient as an alternative or complement to setting - `PG_BIN_DIR` manually (see the `select_pg_path` cross-platform fix). -- `DATA_PATH` should point to a writable directory; create it beforehand - if it doesn't exist yet (`mkdir D:\pg_tools\agent\target\debug\data`). -- Save the block above as e.g. `run-agent.bat` next to the executable - for repeatable local runs. - -### Using a `.env` file instead - -Manually `set`-ing variables in a batch file works, but is easy to lose -track of and doesn't play well with version control hygiene (secrets -end up in shell history). If the agent does not yet support loading a -`.env` file, consider adding support via the [`dotenvy`](https://docs.rs/dotenvy) -crate (maintained successor of `dotenv`) early in `main()`: - -```rust -fn main() { - // Loads variables from a `.env` file in the current directory, if - // present. Existing environment variables are not overridden, so - // this is safe to call even when variables are already set - // externally (e.g. by a process manager or CI). - let _ = dotenvy::dotenv(); - - // ... existing startup code -} -``` - -```toml -[dependencies] -dotenvy = "0.15" -``` - -Example `.env` file (place next to the executable, do **not** commit -this file — add `.env` to `.gitignore`): - -```env -TZ=Europe/Berlin -POLLING=60 -APP_ENV=production -DATA_PATH=D:\pg_tools\agent\target\debug\data -CELERY_BROKER_URL=redis://localhost:65515/ -``` - -Note that `.env` files are a convenient alternative to the `set` -commands above, but won't run `pg_env.bat` for you — either keep that -`call` in a small wrapper script, or set `PG_BIN_DIR` directly in the -`.env` file once the cross-platform `select_pg_path` fix is in place, -e.g.: - -```env -PG_BIN_DIR=C:\Program Files\PostgreSQL\16\bin -``` - -## 5. Start the agent - -With Redis running and the environment configured (either via the batch -script or a `.env` file): - -```powershell -.\portabase-agent.exe -``` - -## Troubleshooting - -- **`openssl-sys` build fails / hangs on `nmake`**: use the vcpkg - approach above instead of the `vendored` feature. -- **`pg_dump`/`pg_restore` not found**: make sure either `pg_env.bat` - was called in the current shell, `PG_BIN_DIR` is set, or PostgreSQL - is installed at the default `C:\Program Files\PostgreSQL\\bin` - location. -- **Redis connection refused**: confirm `redis-server.exe` is running - and that `CELERY_BROKER_URL` matches the port it's listening on. diff --git a/src/domain/postgres/connection.rs b/src/domain/postgres/connection.rs index 16c21bd..a8bb098 100644 --- a/src/domain/postgres/connection.rs +++ b/src/domain/postgres/connection.rs @@ -1,5 +1,6 @@ use crate::domain::postgres::format::PostgresDumpFormat; use crate::services::config::DatabaseConfig; +use crate::settings::CONFIG; use anyhow::Result; use std::path::Path; use tokio_postgres::{Client, Config, NoTls}; @@ -47,11 +48,22 @@ pub async fn server_version(cfg: &DatabaseConfig) -> Result { /// 4. The historical Debian/Ubuntu path as a last-resort fallback, so the /// function keeps returning a `PathBuf` (never panics) even when nothing /// was found, preserving the previous behavior for callers. +/// +/// The override is sourced from `CONFIG.pg_bin_dir` (the `PG_BIN_DIR` +/// environment variable). An empty value means "unset" and falls through to +/// detection. pub fn select_pg_path(version: &str) -> std::path::PathBuf { + select_pg_path_with(version, &CONFIG.pg_bin_dir) +} + +/// Inner resolver behind [`select_pg_path`], parameterized over the +/// `PG_BIN_DIR` override. Kept pure (no env / no `CONFIG` access) so it is +/// unit-testable without mutating process-global state. +pub(crate) fn select_pg_path_with(version: &str, pg_bin_dir: &str) -> std::path::PathBuf { let major = version.split('.').next().unwrap_or("17"); - if let Ok(dir) = std::env::var("PG_BIN_DIR") { - return dir.into(); + if !pg_bin_dir.is_empty() { + return pg_bin_dir.into(); } let candidates: Vec = if cfg!(target_os = "windows") { @@ -89,7 +101,7 @@ pub fn select_pg_path(version: &str) -> std::path::PathBuf { format!("/usr/lib/postgresql/{}/bin", major).into() } -fn pg_dump_binary_name() -> &'static str { +pub(crate) fn pg_dump_binary_name() -> &'static str { if cfg!(target_os = "windows") { "pg_dump.exe" } else { @@ -97,7 +109,7 @@ fn pg_dump_binary_name() -> &'static str { } } -fn pg_dump_exists_in(dir: &std::path::Path) -> bool { +pub(crate) fn pg_dump_exists_in(dir: &std::path::Path) -> bool { dir.join(pg_dump_binary_name()).is_file() } @@ -136,84 +148,6 @@ pub fn detect_format_from_file(restore_file: &Path) -> PostgresDumpFormat { } } -#[cfg(test)] -mod select_pg_path_tests { - use super::*; - use std::sync::Mutex; - - // `std::env::set_var`/`remove_var` are process-global and, as of the - // 2024 edition, marked `unsafe` because mutating them concurrently - // from multiple threads is undefined behavior. Rust runs tests in - // parallel by default, so without serializing access here, these - // tests could race against each other over `PG_BIN_DIR`. - static ENV_LOCK: Mutex<()> = Mutex::new(()); - - // Tests below intentionally avoid asserting on whether a *real* - // PostgreSQL install is or isn't found on the machine running the - // tests (CI runners and developer machines may or may not have one, - // at any version) — that would make the tests environment-dependent - // and flaky. Instead, `PG_BIN_DIR` is always set to a deterministic - // value so behavior doesn't depend on the local system. - - #[test] - fn respects_pg_bin_dir_override() { - let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - let custom = if cfg!(target_os = "windows") { - r"C:\custom\pg\bin" - } else { - "/custom/pg/bin" - }; - // SAFETY: serialized via ENV_LOCK. - unsafe { - std::env::set_var("PG_BIN_DIR", custom); - } - let path = select_pg_path("16.4"); - // SAFETY: serialized via ENV_LOCK. - unsafe { - std::env::remove_var("PG_BIN_DIR"); - } - assert_eq!(path, std::path::PathBuf::from(custom)); - } - - #[test] - fn pg_bin_dir_override_ignores_requested_version() { - // The override is taken as-is, regardless of which version was - // requested — this documents/locks in that behavior. - let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - let custom = if cfg!(target_os = "windows") { - r"C:\custom\pg\bin" - } else { - "/custom/pg/bin" - }; - // SAFETY: serialized via ENV_LOCK. - unsafe { - std::env::set_var("PG_BIN_DIR", custom); - } - let path = select_pg_path("not-a-version"); - // SAFETY: serialized via ENV_LOCK. - unsafe { - std::env::remove_var("PG_BIN_DIR"); - } - assert_eq!(path, std::path::PathBuf::from(custom)); - } - - #[test] - fn pg_dump_binary_name_is_platform_specific() { - let name = pg_dump_binary_name(); - if cfg!(target_os = "windows") { - assert_eq!(name, "pg_dump.exe"); - } else { - assert_eq!(name, "pg_dump"); - } - } - - #[test] - fn pg_dump_exists_in_is_false_for_nonexistent_dir() { - let dir = std::path::Path::new("this/path/almost-certainly/does-not-exist-12345"); - assert!(!pg_dump_exists_in(dir)); - } -} - pub async fn detect_format_from_size(cfg: &DatabaseConfig) -> PostgresDumpFormat { info!( "Detecting database format {:?} - {:?}", diff --git a/src/domain/postgres/mod.rs b/src/domain/postgres/mod.rs index cd42ace..c7cd4cb 100644 --- a/src/domain/postgres/mod.rs +++ b/src/domain/postgres/mod.rs @@ -1,5 +1,5 @@ pub mod backup; -mod connection; +pub(crate) mod connection; pub mod database; mod format; mod ping; diff --git a/src/settings.rs b/src/settings.rs index 039ea70..b4076d1 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -11,6 +11,7 @@ pub struct Settings { pub edge_key: String, pub databases_config_file: String, pub data_path: String, + pub pg_bin_dir: String, pub pooling: usize, pub timezone: String, pub log: String, @@ -59,6 +60,7 @@ impl Settings { databases_config_file: env::var("DATABASES_CONFIG_FILE") .unwrap_or_else(|_| "config.json".into()), data_path: env::var("DATA_PATH").unwrap_or_else(|_| "/config".into()), + pg_bin_dir: env::var("PG_BIN_DIR").unwrap_or_default(), pooling: pooling_seconds, timezone: tz, log: env::var("LOG").unwrap_or_else(|_| "info".into()), diff --git a/src/tests/domain/postgres.rs b/src/tests/domain/postgres.rs index bd4a5a5..04d4cde 100644 --- a/src/tests/domain/postgres.rs +++ b/src/tests/domain/postgres.rs @@ -147,3 +147,64 @@ async fn postgres_password_with_slash_test() { assert_eq!(reachable, true); } + +mod select_pg_path_tests { + use crate::domain::postgres::connection::{ + pg_dump_binary_name, pg_dump_exists_in, select_pg_path_with, + }; + + // `select_pg_path_with` takes the `PG_BIN_DIR` override as a plain + // argument, so these tests never touch process-global env state or the + // cached `CONFIG`. They stay deterministic regardless of whether — or at + // which version — a real PostgreSQL install exists on the host. + + #[test] + fn respects_pg_bin_dir_override() { + let custom = if cfg!(target_os = "windows") { + r"C:\custom\pg\bin" + } else { + "/custom/pg/bin" + }; + let path = select_pg_path_with("16.4", custom); + assert_eq!(path, std::path::PathBuf::from(custom)); + } + + #[test] + fn pg_bin_dir_override_ignores_requested_version() { + // The override is taken as-is, regardless of which version was + // requested — this documents/locks in that behavior. + let custom = if cfg!(target_os = "windows") { + r"C:\custom\pg\bin" + } else { + "/custom/pg/bin" + }; + let path = select_pg_path_with("not-a-version", custom); + assert_eq!(path, std::path::PathBuf::from(custom)); + } + + #[test] + fn empty_pg_bin_dir_falls_through_to_detection() { + // An empty override means "unset" (matches `CONFIG.pg_bin_dir` when + // `PG_BIN_DIR` is absent). It must not be returned as a literal empty + // path — resolution falls through to platform defaults / PATH lookup + // and yields a non-empty path. + let path = select_pg_path_with("17", ""); + assert_ne!(path, std::path::PathBuf::from("")); + } + + #[test] + fn pg_dump_binary_name_is_platform_specific() { + let name = pg_dump_binary_name(); + if cfg!(target_os = "windows") { + assert_eq!(name, "pg_dump.exe"); + } else { + assert_eq!(name, "pg_dump"); + } + } + + #[test] + fn pg_dump_exists_in_is_false_for_nonexistent_dir() { + let dir = std::path::Path::new("this/path/almost-certainly/does-not-exist-12345"); + assert!(!pg_dump_exists_in(dir)); + } +} From f02218708ab6b1fdf8df983135d2e04954ae4d19 Mon Sep 17 00:00:00 2001 From: Charles GTE Date: Sat, 20 Jun 2026 12:52:53 +0200 Subject: [PATCH 6/6] fix: windows-release.yml --- .github/workflows/windows-release.yml | 17 +++++++---------- src/domain/postgres/database.rs | 1 - 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index 5f566e9..c5e5cf3 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -44,7 +44,8 @@ jobs: run: | # Ensure the environment variable is present for this step if (-Not $env:OPENSSL_DIR) { Write-Host "OPENSSL_DIR not set, printing env for debugging"; Get-ChildItem Env: | ForEach-Object { Write-Host $_ } } - cargo build --release + # Build the declared bin target explicitly (Cargo.toml [[bin]] name = "app") + cargo build --release --bin app - name: Prepare artifact zip id: prepare_artifact @@ -55,18 +56,14 @@ jobs: $tag = $env:RELEASE_TAG if (-not $tag) { $tag = $env:GITHUB_SHA } - # Find the built executable. Prefer an explicit portabase-agent.exe, fall back to first exe in target/release - $exe = "target\release\portabase-agent.exe" - if (-not (Test-Path $exe)) { - $first = Get-ChildItem -Path target\release -Filter *.exe | Select-Object -First 1 - if ($first) { $exe = $first.FullName } - } - - if (-not (Test-Path $exe)) { Write-Error "Built binary not found in target/release"; exit 1 } + # Package the declared bin target deterministically (Cargo.toml [[bin]] name = "app") + $exe = "target\release\app.exe" + if (-not (Test-Path $exe)) { Write-Error "Built binary $exe not found in target/release"; exit 1 } $outDir = "artifact" New-Item -ItemType Directory -Path $outDir -Force | Out-Null - Copy-Item -Path $exe -Destination $outDir\ + # Ship under the package name, not the internal bin name "app" + Copy-Item -Path $exe -Destination "$outDir\portabase-agent.exe" $zipName = "windows-release-$tag.zip" if (Test-Path $zipName) { Remove-Item $zipName } diff --git a/src/domain/postgres/database.rs b/src/domain/postgres/database.rs index c10b114..af18f81 100644 --- a/src/domain/postgres/database.rs +++ b/src/domain/postgres/database.rs @@ -23,7 +23,6 @@ impl PostgresDatabase { fn build_env(&self) -> HashMap { let mut envs = std::env::vars().collect::>(); envs.insert("PGPASSWORD".to_string(), self.cfg.password.to_string()); - info!("envs: {:?}", envs); envs } }