diff --git a/.github/workflows/node-spanner-lib-wrapper-unit-tests.yml b/.github/workflows/node-spanner-lib-wrapper-unit-tests.yml index 026eb7e09..fd43b7238 100644 --- a/.github/workflows/node-spanner-lib-wrapper-unit-tests.yml +++ b/.github/workflows/node-spanner-lib-wrapper-unit-tests.yml @@ -3,12 +3,8 @@ name: Node Wrapper Unit Tests on: push: branches: [ "main" ] - paths: - - 'spannerlib/wrappers/spannerlib-node/**' pull_request: branches: [ "main" ] - paths: - - 'spannerlib/wrappers/spannerlib-node/**' workflow_dispatch: permissions: @@ -21,25 +17,33 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + # Use ubuntu-22.04 (glibc 2.35) for Linux to ensure broad binary compatibility. + # Pin to windows-2022 (Visual Studio 2022) to avoid node-gyp auto-detection failure on experimental VS 18 preview on windows-latest. + os: [ubuntu-22.04, macos-latest, windows-2022] node-version: [22, 24, 26] + env: + npm_config_enable_thin_lto: "false" + npm_config_enable_lto: "false" + npm_config_use_lld: "false" + GYP_DEFINES: "enable_thin_lto=false enable_lto=false use_lld=false lto_jobs=" + defaults: run: shell: bash working-directory: ./spannerlib/wrappers/spannerlib-node steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Go - uses: actions/setup-go@v6 + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 with: go-version: '1.26.x' cache-dependency-path: spannerlib/go.sum - name: Set up Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ matrix.node-version }} cache: 'npm' @@ -49,7 +53,9 @@ jobs: run: npm install - name: Build Addon and TS - run: npm run build + run: | + unset CFLAGS CXXFLAGS LDFLAGS 2>/dev/null || true + npm run build - name: Run Unit Tests run: npm test diff --git a/.github/workflows/release-node-wrapper.yml b/.github/workflows/release-node-wrapper.yml new file mode 100644 index 000000000..ef15dd562 --- /dev/null +++ b/.github/workflows/release-node-wrapper.yml @@ -0,0 +1,204 @@ +name: Build and Release Node Wrapper + +on: + workflow_dispatch: + inputs: + platform: + description: 'Target platform package to release' + required: true + type: choice + options: + - 'all' + - 'darwin-arm64' + - 'linux-x64' + - 'linux-arm64' + - 'win32-x64' + default: 'all' + npm_tag: + description: 'NPM distribution tag (e.g. alpha(default), beta, latest, next)' + required: false + type: string + default: 'alpha' + npm_token: + description: 'NPM Token for Wombat (leave empty for dry run)' + required: false + type: string + default: '' + +permissions: + contents: read + +jobs: + release: + name: Release ${{ matrix.pkg_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + target: darwin-arm64 + os_name: darwin + cpu_name: arm64 + goarch: arm64 + pkg_name: "@google-cloud/spannerlib-node-darwin-arm64" + - os: ubuntu-22.04 + target: linux-x64 + os_name: linux + cpu_name: x64 + goarch: amd64 + pkg_name: "@google-cloud/spannerlib-node-linux-x64" + - os: ubuntu-22.04 + target: linux-arm64 + os_name: linux + cpu_name: arm64 + goarch: arm64 + pkg_name: "@google-cloud/spannerlib-node-linux-arm64" + cc: aarch64-linux-gnu-gcc + cxx: aarch64-linux-gnu-g++ + ar: aarch64-linux-gnu-ar + - os: windows-2022 + target: win32-x64 + os_name: win32 + cpu_name: x64 + goarch: amd64 + pkg_name: "@google-cloud/spannerlib-node-win32-x64" + + defaults: + run: + shell: bash + working-directory: ./spannerlib/wrappers/spannerlib-node + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Check Platform Target + id: check + env: + PLATFORM_INPUT: ${{ inputs.platform || 'all' }} + MATRIX_TARGET: ${{ matrix.target }} + run: | + if [ "$PLATFORM_INPUT" = "all" ] || [ "$PLATFORM_INPUT" = "$MATRIX_TARGET" ]; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + echo "Skipping matrix target $MATRIX_TARGET (requested: $PLATFORM_INPUT)" + fi + + - name: Install ARM64 Cross-Compiler (Linux ARM64) + if: steps.check.outputs.run == 'true' && matrix.target == 'linux-arm64' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu binutils-aarch64-linux-gnu + + - name: Set up Go + if: steps.check.outputs.run == 'true' + uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + with: + go-version: '1.26.x' + cache-dependency-path: spannerlib/go.sum + + - name: Set up Node.js + if: steps.check.outputs.run == 'true' + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: spannerlib/wrappers/spannerlib-node/package.json + + - name: Mask NPM Token + if: steps.check.outputs.run == 'true' && inputs.npm_token != '' + env: + NPM_TOKEN: ${{ inputs.npm_token }} + run: | + echo "::add-mask::${NPM_TOKEN}" + + - name: Configure NPM registry auth + if: steps.check.outputs.run == 'true' && inputs.npm_token != '' + env: + NPM_TOKEN: ${{ inputs.npm_token }} + run: | + echo "//wombat-dressing-room.appspot.com/:_authToken=${NPM_TOKEN}" > ~/.npmrc + + - name: Install dependencies + if: steps.check.outputs.run == 'true' + run: npm install + + - name: Build Go library and C++ Addon + if: steps.check.outputs.run == 'true' + env: + npm_config_enable_thin_lto: "false" + npm_config_enable_lto: "false" + npm_config_use_lld: "false" + GYP_DEFINES: "enable_thin_lto=false enable_lto=false use_lld=false lto_jobs=" + npm_config_arch: ${{ matrix.cpu_name }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: "1" + run: | + unset CFLAGS CXXFLAGS LDFLAGS 2>/dev/null || true + [ -n "${{ matrix.cc }}" ] && export CC="${{ matrix.cc }}" + [ -n "${{ matrix.cxx }}" ] && export CXX="${{ matrix.cxx }}" + [ -n "${{ matrix.ar }}" ] && export AR="${{ matrix.ar }}" + npm run build + + - name: Prepare Platform Package Manifest + if: steps.check.outputs.run == 'true' + env: + PKG_NAME: ${{ matrix.pkg_name }} + OS_NAME: ${{ matrix.os_name }} + CPU_NAME: ${{ matrix.cpu_name }} + run: | + node -e ' + const fs = require("fs"); + const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8")); + pkg.name = process.env.PKG_NAME; + pkg.os = [process.env.OS_NAME]; + pkg.cpu = [process.env.CPU_NAME]; + fs.writeFileSync("./package.json", JSON.stringify(pkg, null, 2) + "\n"); + console.log("Updated package.json for " + pkg.name + " (" + pkg.os + "/" + pkg.cpu + ")"); + ' + + - name: Package and Publish + if: steps.check.outputs.run == 'true' + env: + NPM_TOKEN: ${{ inputs.npm_token }} + NPM_TAG: ${{ inputs.npm_tag || 'alpha' }} + run: | + set -eo pipefail + export NPM_CONFIG_PREFIX="${HOME}/.npm-global" + + # Optional releasetool publisher script reporter if available + if python3 -c "import releasetool" 2>/dev/null; then + python3 -m releasetool publish-reporter-script > /tmp/publisher-script 2>/dev/null || true + if [ -f /tmp/publisher-script ]; then + source /tmp/publisher-script || true + fi + fi + + npm pack . + TARBALL=$(ls -1 -t *.tgz | head -1) + echo "Generated tarball: $TARBALL" + + # Publish only when a non-empty npm_token is provided + if [ -n "$NPM_TOKEN" ]; then + echo "Publishing $TARBALL with tag '$NPM_TAG' to Wombat registry..." + npm publish --access=public --tag "$NPM_TAG" --registry=https://wombat-dressing-room.appspot.com "$TARBALL" + else + echo "Dry run mode active: No NPM token provided. Skipped publishing $TARBALL." + fi + + find node_modules -name package-lock.json -o -name "*.tgz" | xargs rm -f 2>/dev/null || true + + - name: Upload Release Tarball Artifact + if: steps.check.outputs.run == 'true' + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + with: + name: spannerlib-node-${{ matrix.target }}-tarball + path: spannerlib/wrappers/spannerlib-node/*.tgz + + - name: Cleanup credentials + if: always() + run: | + rm -f ~/.npmrc + rm -f /tmp/publisher-script 2>/dev/null || true diff --git a/spannerlib/wrappers/spannerlib-node/BUILD_AND_RELEASE.md b/spannerlib/wrappers/spannerlib-node/BUILD_AND_RELEASE.md index c0b61724f..1afc1fb84 100644 --- a/spannerlib/wrappers/spannerlib-node/BUILD_AND_RELEASE.md +++ b/spannerlib/wrappers/spannerlib-node/BUILD_AND_RELEASE.md @@ -15,25 +15,27 @@ Bridging JavaScript to the native Go SDK involves a sequential compilation pipel | (cgo linker bridge) v - [Go Shared Library] + [Go Static Archive / DLL] ``` ## Compilation Phases -### Phase 1: Compiling the Go Shared Library (CGO Link) +### Phase 1: Compiling the Go Library (CGO Link) Before building the Node.js Addon, the underlying Go codebase must be compiled into an object format that C/C++ can link against. * **Trigger:** Executed via `npm run build:go` which runs `bash scripts/build-shared-lib.sh`. -* **Action:** The build script invokes the Go compiler with the `-buildmode=c-shared` flag, targeting the primary C-shared entry point located at [shared_lib.go](../../shared/shared_lib.go). -* **Outputs:** Generates a platform-specific native shared library (e.g., `libspannerlib.dylib` on macOS, `.so` on Linux, `.dll` on Windows) along with the corresponding C header file (`libspannerlib.h`). Both files are placed into the `spannerlib/shared/` directory. +* **Action & Outputs:** + * **Linux & macOS (`-buildmode=c-archive`):** Invokes Go with `-buildmode=c-archive` targeting [shared_lib.go](../../shared/shared_lib.go) to generate a static archive (`libspanner.a`) and C header (`libspanner.h`). The static archive embeds all Go runtime and driver symbols directly into the final `spanner_napi.node` binary, eliminating dynamic shared library dependencies, `@loader_path` rpaths, and `.so`/`.dylib` file distribution. + * **Windows (`-buildmode=c-shared`):** Invokes Go with `-buildmode=c-shared` to generate a dynamic shared library (`libspanner.dll`) along with a companion MSVC-compatible import library (`libspanner.lib`) required by MSVC's linker (`link.exe`). ### Phase 2: Compiling the Native Bridge (node-gyp) -Once the Go shared library is generated, the Node.js C++ wrapper is compiled using `node-gyp` to map V8 engine objects into Go pointers. +Once the Go library is generated, the Node.js C++ wrapper is compiled using `node-gyp` to map V8 engine objects into Go pointers. * **Trigger:** Executed as part of `npm run build` which invokes `node-gyp rebuild`. -* **Action:** Reads the `gyp` build instructions in [binding.gyp](./binding.gyp) to locate the Go header files, and dynamically links the bridge against the generated Go shared object. It compiles the bridge source file [addon.cc](./src/cpp/addon.cc) using the local OS C++ compiler toolchain (e.g., Clang on macOS, GCC on Linux, MSVC on Windows). +* **Action:** Reads the build instructions in [binding.gyp](./binding.gyp) to locate the Go header files, and links the bridge against the generated Go library. It compiles the bridge source file [addon.cc](./src/cpp/addon.cc) using the local OS C++ compiler toolchain (Clang on macOS, GCC on Linux, MSVC on Windows). + * On **Linux and macOS**, `binding.gyp` statically links `libspanner.a` directly into the `.node` binary. + * On **Windows**, `binding.gyp` links against `libspanner.lib` and copies `libspanner.dll` adjacent to the addon in `build/Release/`. * **Output:** Generates the native Node.js binary file at `build/Release/spanner_napi.node`. -* **Post-build Link Patch (macOS Only):** To ensure portability on macOS without requiring root or global library installs, `npm run postbuild` invokes the `install_name_tool`. This command alters the dynamic linker search path in the `.node` file to use `@loader_path/libspannerlib.dylib`, ensuring Node.js locates the Go dynamic library relatively from the compiled C++ bridge binary path. ### Phase 3: TypeScript Compilation & Dual-Publishing (ESM / CJS) @@ -51,4 +53,42 @@ To run the entire pipeline end-to-end and generate a fully runnable local build, ```bash npm run build ``` -This builds the underlying Go shared library, links the C++ bridge layer via `node-gyp`, patches dynamic linker paths, and outputs the final dual ESM/CJS JavaScript distributions. +This builds the underlying Go library, links the C++ bridge layer via `node-gyp`, and outputs the final dual ESM/CJS JavaScript distributions. + +## Platform-Specific Release Pipelines (GitHub Actions) + +Releasing the prebuilt native platform packages is managed via a unified manual GitHub Actions workflow: +* **Workflow:** [release-node-wrapper.yml](../../../.github/workflows/release-node-wrapper.yml) + +### Target Platform Packages + +| Package Name | Target Platform | Runner OS | glibc / Toolchain | +| :--- | :--- | :--- | :--- | +| **`@google-cloud/spannerlib-node-darwin-arm64`** | macOS (Apple Silicon `arm64`) | `macos-latest` | Apple Clang (`c-archive` static) | +| **`@google-cloud/spannerlib-node-linux-x64`** | Linux (`x64`) | `ubuntu-22.04` | **glibc 2.35** / GCC (`c-archive` static) | +| **`@google-cloud/spannerlib-node-linux-arm64`** | Linux (`arm64`) | `ubuntu-22.04` | **glibc 2.35** / `gcc-aarch64-linux-gnu` (cross-compile) | +| **`@google-cloud/spannerlib-node-win32-x64`** | Windows (`x64`) | `windows-2022` | MSVC 2022 (`c-shared` DLL + `.lib`) | + +> **Note on Linux Compatibility:** Compiling on `ubuntu-22.04` dynamically links against **glibc 2.35**, ensuring wide binary compatibility with older and enterprise Linux distributions (such as Debian 12, Ubuntu 22.04+, and RHEL 9). The Linux ARM64 build is cross-compiled on `ubuntu-22.04` using `gcc-aarch64-linux-gnu` rather than running on Ubuntu 24.04 ARM runners to prevent glibc 2.39 lock-in. + +### Triggering a Release +The workflow uses `workflow_dispatch` and publishes to the Google Wombat registry (`https://wombat-dressing-room.appspot.com`): + +1. Go to the **Actions** tab in GitHub. +2. Select **Build and Release Node Wrapper**. +3. Click **Run workflow**. +4. Provide the inputs: + * **`platform`** *(Required)*: Select `all` (default) to build and release all 4 platforms concurrently, or select a specific target (`darwin-arm64`, `linux-x64`, `linux-arm64`, `win32-x64`). + * **`npm_tag`** *(Optional, default: `alpha`)*: NPM distribution tag (e.g. `alpha`, `beta`, `latest`). + * **`npm_token`** *(Optional)*: The authentication token for `//wombat-dressing-room.appspot.com/:_authToken`. If left empty, the workflow automatically runs in **dry-run mode** (builds, packages, and uploads `.tgz` artifacts without publishing). + +### Release Execution Steps +For each target platform in the matrix, the workflow: +1. Sets up Go (`1.26.x`) and Node.js (`22`) environments. +2. Masks the supplied `npm_token` and configures `~/.npmrc` for Wombat registry auth. +3. Installs dependencies (`npm install`). +4. Compiles the Go library (`libspanner.a` / `libspanner.dll`), links the C++ N-API addon, and compiles TypeScript dual outputs (`npm run build`). +5. Sets the target package name (e.g. `@google-cloud/spannerlib-node-linux-x64`) and `os`/`cpu` metadata in `package.json`. +6. Packages the distribution tarball (`npm pack .`) containing both the JavaScript bundles and the compiled native binary (`spanner_napi.node` plus `libspanner.dll` on Windows). +7. Publishes the generated tarball to Wombat with the specified dist-tag (`npm publish --access=public --tag $NPM_TAG --registry=https://wombat-dressing-room.appspot.com`). +8. Archives and uploads the release `.tgz` as a workflow artifact for auditing and SBOM generation. diff --git a/spannerlib/wrappers/spannerlib-node/README.md b/spannerlib/wrappers/spannerlib-node/README.md index b87dea932..85bd48bb0 100644 --- a/spannerlib/wrappers/spannerlib-node/README.md +++ b/spannerlib/wrappers/spannerlib-node/README.md @@ -1,6 +1,6 @@ # Node-API Wrapper for Spanner Shared Library -This package provides a high-performance Node-API (N-API) bridge to the Go-based Spanner shared library. It offers superior stability and performance compared to traditional FFI approaches. +> **NOTICE:** This is an internal library intended for use by Google Cloud Spanner driver packages (such as the high-level Node.js Spanner driver). It is not intended for direct use by end customers and can introduce breaking changes without prior notice. ## Prerequisites diff --git a/spannerlib/wrappers/spannerlib-node/binding.gyp b/spannerlib/wrappers/spannerlib-node/binding.gyp index e2457df3e..e8817ce34 100644 --- a/spannerlib/wrappers/spannerlib-node/binding.gyp +++ b/spannerlib/wrappers/spannerlib-node/binding.gyp @@ -1,64 +1,88 @@ { + 'variables': { + 'enable_thin_lto%': 'false', + 'enable_lto%': 'false', + 'use_lld%': 'false', + 'lto_jobs%': '' + }, + 'target_defaults': { + 'conditions': [ + ['OS=="win"', { + 'msvs_settings': { + 'VCCLCompilerTool': { + 'ExceptionHandling': 1, + 'EnablePREfast': 'false', + 'LanguageStandard': 'stdcpp20', + 'AdditionalOptions!': [ + '-flto=thin', + '/flto=thin', + '-flto', + '/flto' + ] + }, + 'VCLinkerTool': { + 'LinkTimeCodeGeneration': 0, + 'AdditionalOptions!': [ + '/opt:lldltojobs=1', + '/opt:lldltojobs=2', + '/opt:lldltojobs=4', + '/opt:lldltojobs=8', + '-flto=thin', + '/flto=thin', + '-flto', + '/flto' + ] + } + } + }] + ] + }, 'targets': [ { 'target_name': 'spanner_napi', 'sources': [ 'src/cpp/addon.cc' ], 'include_dirs': [ '=20.0.0" + "node": ">=22" }, "scripts": { "build:go": "bash scripts/build-shared-lib.sh", "build": "npm run build:go && node-gyp rebuild && npm run compile", - "postbuild": "node -e \"if (process.platform === 'darwin') require('child_process').execSync('install_name_tool -change libspanner.dylib @loader_path/libspanner.dylib ./build/Release/spanner_napi.node')\"", "compile:esm": "tsc -p .", "compile:cjs": "tsc -p ./tsconfig.cjs.json && babel build/cjs --out-dir build/cjs --out-file-extension .cjs && node scripts/fix-extensions.cjs", "compile": "npm run compile:esm && npm run compile:cjs", @@ -39,14 +46,15 @@ "files": [ "build/esm/src", "build/cjs/src", - "build/Release/*.node" + "build/Release/*.node", + "build/Release/libspanner.dll" ], "dependencies": { - "node-addon-api": "^8.0.0", "bindings": "^1.5.0", "@google-cloud/spanner": "^8.7.1" }, "devDependencies": { + "node-addon-api": "^8.0.0", "mocha": "^11.0.1", "typescript": "^6.0.0", "@types/node": "^24.0.0", diff --git a/spannerlib/wrappers/spannerlib-node/scripts/build-shared-lib.sh b/spannerlib/wrappers/spannerlib-node/scripts/build-shared-lib.sh index d8b5d0279..baea4f500 100755 --- a/spannerlib/wrappers/spannerlib-node/scripts/build-shared-lib.sh +++ b/spannerlib/wrappers/spannerlib-node/scripts/build-shared-lib.sh @@ -15,14 +15,17 @@ set -e # See the License for the specific language governing permissions and # limitations under the License. -# Builds the shared library and places it in the shared directory. -# This script handles OS detection to use the correct file extension. +# Builds the Go library for the Node wrapper. +# - On Linux and macOS: Builds a static archive (c-archive) so that it is embedded +# directly into spanner_napi.node without requiring dynamic library shipping or rpath. +# - On Windows: Builds a shared DLL (c-shared) and companion import library (.lib) +# as required by MSVC link.exe. log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" } -log "Starting Spannerlib Shared Library Build for Node Wrapper..." +log "Starting Spannerlib Library Build for Node Wrapper..." # Resolve absolute paths SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -41,14 +44,39 @@ esac log "Auto-detected OS: $OS" if [ "$OS" == "macOS" ]; then - echo "Building for macOS..." - go build -C "$SHARED_LIB_DIR" -o libspanner.dylib -buildmode=c-shared shared_lib.go + echo "Building static archive for macOS (c-archive)..." + go build -C "$SHARED_LIB_DIR" -o libspanner.a -buildmode=c-archive shared_lib.go elif [ "$OS" == "Linux" ]; then - echo "Building for Linux..." - go build -C "$SHARED_LIB_DIR" -o libspanner.so -buildmode=c-shared shared_lib.go + echo "Building static archive for Linux (c-archive)..." + go build -C "$SHARED_LIB_DIR" -o libspanner.a -buildmode=c-archive shared_lib.go elif [ "$OS" == "Windows" ]; then - echo "Building for Windows..." - go build -C "$SHARED_LIB_DIR" -o libspanner.dll -buildmode=c-shared shared_lib.go + echo "Building shared library for Windows (c-shared)..." + ( + cd "$SHARED_LIB_DIR" || exit 1 + # 1. Build DLL with Go + go build -o libspanner.dll -buildmode=c-shared shared_lib.go + + # 2. Extract symbol definitions (dumpbin with gendef fallback) + if (command -v dumpbin.exe >/dev/null 2>&1 || command -v dumpbin >/dev/null 2>&1); then + echo "Extracting exports using dumpbin..." + echo "EXPORTS" > libspanner.def + dumpbin /EXPORTS libspanner.dll | awk '/ordinal hint/,/Summary/' | awk '{print $4}' | grep -v '^$' | grep -v 'Summary' >> libspanner.def || true + elif command -v gendef >/dev/null 2>&1; then + echo "Extracting exports using gendef..." + gendef libspanner.dll + fi + + # 3. Create MSVC-compatible .lib using MSVC's lib.exe (with dlltool fallback) + if (command -v lib.exe >/dev/null 2>&1 || command -v lib >/dev/null 2>&1) && [ -s "libspanner.def" ]; then + echo "Generating MSVC-compatible libspanner.lib with lib.exe..." + lib.exe /def:libspanner.def /out:libspanner.lib /machine:X64 2>/dev/null || lib /def:libspanner.def /out:libspanner.lib /machine:X64 + rm -f libspanner.exp libspanner.def + elif command -v dlltool >/dev/null 2>&1 && [ -s "libspanner.def" ]; then + echo "Generating libspanner.lib using dlltool fallback..." + dlltool -D libspanner.dll -d libspanner.def -l libspanner.lib -m i386:x86-64 + rm -f libspanner.def + fi + ) else echo "Unsupported operating system: $OS" exit 1