diff --git a/.github/docker/AddLibmongocrypt.dockerfile b/.github/docker/AddLibmongocrypt.dockerfile new file mode 100644 index 0000000..94d2297 --- /dev/null +++ b/.github/docker/AddLibmongocrypt.dockerfile @@ -0,0 +1,9 @@ +ARG ARCH +ARG NODE_VERSION +FROM ${ARCH}-alpine-base-node-${NODE_VERSION}:latest + +WORKDIR /mongodb-client-encryption +COPY . . + +RUN npm install --ignore-scripts +RUN npm run install:libmongocrypt -- --skip-bindings diff --git a/.github/docker/Dockerfile.glibc b/.github/docker/Dockerfile.glibc index cfd02fc..fc4225d 100644 --- a/.github/docker/Dockerfile.glibc +++ b/.github/docker/Dockerfile.glibc @@ -11,13 +11,11 @@ ENV PATH=$PATH:/nodejs/bin WORKDIR /mongodb-client-encryption COPY . . -RUN apt-get -qq update && apt-get -qq install -y python3 build-essential git && ldd --version +RUN apt-get -qq update && apt-get -qq install -y build-essential curl git && ldd --version -RUN npm run install:libmongocrypt - -ARG RUN_TEST -RUN if [ -n "$RUN_TEST" ]; then npm test ; else echo "skipping tests" ; fi - -FROM scratch - -COPY --from=build /mongodb-client-encryption/prebuilds/ / +RUN bash install-pyenv.sh +RUN pyenv install 3.10 && pyenv global 3.10 +RUN python --version +RUN git --version +RUN c++ --version +RUN g++ --version diff --git a/.github/docker/Dockerfile.musl b/.github/docker/Dockerfile.musl new file mode 100644 index 0000000..3bbec2e --- /dev/null +++ b/.github/docker/Dockerfile.musl @@ -0,0 +1,11 @@ +ARG ARCH=arm64 +ARG NODE_VERSION=16.20.1 + +FROM ${ARCH}/node:${NODE_VERSION}-alpine AS dependencies + +RUN apk --no-cache add make g++ libc-dev curl bash python3 py3-pip cmake git + +RUN python3 --version +RUN git --version +RUN c++ --version +RUN g++ --version diff --git a/.github/docker/Prebuilds.dockerfile b/.github/docker/Prebuilds.dockerfile new file mode 100644 index 0000000..1c80ab3 --- /dev/null +++ b/.github/docker/Prebuilds.dockerfile @@ -0,0 +1,5 @@ +ARG ARCH +ARG NODE_VERSION +FROM ${ARCH}-alpine-libmongocrypt-node-${NODE_VERSION}:latest + +RUN npm run prebuild diff --git a/.github/scripts/utils.mjs b/.github/scripts/utils.mjs index 1a54385..45d408d 100644 --- a/.github/scripts/utils.mjs +++ b/.github/scripts/utils.mjs @@ -1,10 +1,10 @@ // @ts-check -import { execSync } from "child_process"; import path from "path"; import url from 'node:url'; import { spawn } from "node:child_process"; import { once } from "node:events"; +import { execSync } from "child_process"; const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); @@ -55,22 +55,25 @@ export function buildLibmongocryptDownloadUrl(ref, platform) { } export function getLibmongocryptPrebuildName() { - const platformMatrix = { - ['darwin-arm64']: 'macos', - ['darwin-x64']: 'macos', - ['linux-ppc64']: 'rhel-71-ppc64el', - ['linux-s390x']: 'rhel72-zseries-test', - ['linux-arm64']: 'ubuntu1804-arm64', - ['linux-x64']: 'rhel-70-64-bit', - ['win32-x64']: 'windows-test' - }; - - const detectedPlatform = `${process.platform}-${process.arch}`; - const prebuild = platformMatrix[detectedPlatform]; - - if (prebuild == null) throw new Error(`Unsupported: ${detectedPlatform}`); - - return prebuild; + const prebuildIdentifierFactory = { + 'darwin': () => 'macos', + 'win32': () => 'windows-test', + 'linux': () => { + const key = `${getLibc()}-${process.arch}`; + return { + ['musl-x64']: 'alpine-amd64-earthly', + ['musl-arm64']: 'alpine-arm64-earthly', + ['glibc-ppc64']: 'rhel-71-ppc64el', + ['glibc-s390x']: 'rhel72-zseries-test', + ['glibc-arm64']: 'ubuntu1804-arm64', + ['glibc-x64']: 'rhel-70-64-bit', + }[key] + } + }[process.platform] ?? (() => { + throw new Error(`Unsupported platform`); + }); + + return prebuildIdentifierFactory(); } /** `xtrace` style command runner, uses spawn so that stdio is inherited */ @@ -86,4 +89,28 @@ export async function run(command, args = [], options = {}) { await once(proc, 'exit'); if (proc.exitCode != 0) throw new Error(`CRASH(${proc.exitCode}): ${commandDetails}`); -} \ No newline at end of file +} + +/** + * @returns the libc (`musl` or `glibc`), if the platform is linux, otherwise null. + */ +function getLibc() { + if (process.platform !== 'linux') return null; + + /** + * executes `ldd --version`. on Alpine linux, `ldd` and `ldd --version` return exit code 1 and print the version + * info to stderr, but on other platforms, `ldd --version` prints to stdout and returns exit code 0. + * + * So, this script works on both by return stderr if the command returns a non-zero exit code, otherwise stdout. + */ + function lddVersion() { + try { + return execSync('ldd --version', { encoding: 'utf-8' }); + } catch (error) { + return error.stderr; + } + } + + console.error({ ldd: lddVersion() }); + return lddVersion().includes('musl') ? 'musl' : 'glibc'; +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c2e37bd..07c2461 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: retention-days: 1 compression-level: 0 - container_builds: + container_builds_glibc: outputs: artifact_id: ${{ steps.upload.outputs.artifact-id }} runs-on: ubuntu-latest @@ -64,8 +64,48 @@ jobs: name: Upload prebuild uses: actions/upload-artifact@v4 with: - name: build-linux-${{ matrix.linux_arch }} + name: build-linux-glibc-${{ matrix.linux_arch }} path: prebuilds/ if-no-files-found: 'error' retention-days: 1 compression-level: 0 + + container_tests_musl: + runs-on: ubuntu-latest + strategy: + matrix: + linux_arch: [amd64, arm64] + fail-fast: false + steps: + - uses: actions/checkout@v4 + + - name: Get Full Node.js Version + id: get_nodejs_version + shell: bash + run: | + echo "version=$(node --print 'process.version.slice(1)')" >> "$GITHUB_OUTPUT" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Run Buildx + run: | + docker buildx create --name builder --bootstrap --use + docker --debug buildx build --progress=plain --no-cache \ + --platform linux/${{ matrix.linux_arch }} \ + --build-arg="PLATFORM=${{ matrix.linux_arch == 'arm64' && 'arm64v8' || matrix.linux_arch }}" \ + --output type=local,dest=./prebuilds,platform-split=false \ + -f ./.github/docker/Dockerfile.musl \ + . + - id: upload + name: Upload prebuild + uses: actions/upload-artifact@v4 + with: + name: build-linux-musl-${{ matrix.linux_arch }} + path: prebuilds/ + if-no-files-found: "error" + retention-days: 1 + compression-level: 0 \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3c74b60..bdba7d3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,36 +8,82 @@ on: name: Test jobs: - host_tests: - strategy: - matrix: - os: [macos-latest, windows-2019] - node: [16.x, 18.x, 20.x, 22.x] - fail-fast: false - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 + # host_tests: + # strategy: + # matrix: + # os: [macos-latest, windows-2019] + # node: [16.x, 18.x, 20.x, 22.x] + # fail-fast: false + # runs-on: ${{ matrix.os }} + # steps: + # - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node }} - cache: "npm" - registry-url: "https://registry.npmjs.org" + # - uses: actions/setup-node@v4 + # with: + # node-version: ${{ matrix.node }} + # cache: "npm" + # registry-url: "https://registry.npmjs.org" - - name: Build with Node.js ${{ matrix.node }} on ${{ matrix.os }} - run: node .github/scripts/libmongocrypt.mjs ${{ runner.os == 'Windows' && '--build' || '' }} - shell: bash + # - name: Build with Node.js ${{ matrix.node }} on ${{ matrix.os }} + # run: node .github/scripts/libmongocrypt.mjs ${{ runner.os == 'Windows' && '--build' || '' }} + # shell: bash - - name: Test ${{ matrix.os }} - shell: bash - run: npm run test + # - name: Test ${{ matrix.os }} + # shell: bash + # run: npm run test + + # container_tests_glibc: + # runs-on: ubuntu-latest + # strategy: + # matrix: + # linux_arch: [s390x, arm64, amd64] + # node: [16.x, 18.x, 20.x, 22.x] + # fail-fast: false + # steps: + # - uses: actions/checkout@v4 + + # - uses: actions/setup-node@v4 + # with: + # node-version: ${{ matrix.node }} + + # - name: Get Full Node.js Version + # id: get_nodejs_version + # shell: bash + # run: | + # echo "version=$(node --print 'process.version.slice(1)')" >> "$GITHUB_OUTPUT" + # echo "ubuntu_version=$(node --print '(+process.version.slice(1).split(`.`).at(0)) > 16 ? `noble` : `bionic`')" >> "$GITHUB_OUTPUT" + + # - name: Set up QEMU + # uses: docker/setup-qemu-action@v3 + # with: + # image: 'tonistiigi/binfmt@sha256:66e11bea77a5ea9d6f0fe79b57cd2b189b5d15b93a2bdb925be22949232e4e55' - container_tests: + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v3 + # with: + # version: v0.18.0 + # driver-opts: image=moby/buildkit:v0.17.2 + + # - name: Run Buildx + # run: | + # docker buildx create --name builder --bootstrap --use + # docker buildx build \ + # --platform linux/${{ matrix.linux_arch }} \ + # --build-arg="NODE_ARCH=${{ matrix.linux_arch == 'amd64' && 'x64' || matrix.linux_arch }}" \ + # --build-arg="NODE_VERSION=${{ steps.get_nodejs_version.outputs.version }}" \ + # --build-arg="UBUNTU_VERSION=${{ steps.get_nodejs_version.outputs.ubuntu_version }}" \ + # --build-arg="RUN_TEST=true" \ + # --output type=local,dest=./prebuilds,platform-split=false \ + # -f ./.github/docker/Dockerfile.glibc \ + # . + + + container_tests_musl: runs-on: ubuntu-latest strategy: matrix: - linux_arch: [s390x, arm64, amd64] - node: [16.x, 18.x, 20.x, 22.x] + linux_arch: [amd64, arm64] + node: [16.20.1, 18.x, 20.x, 22.x] fail-fast: false steps: - uses: actions/checkout@v4 @@ -51,23 +97,102 @@ jobs: shell: bash run: | echo "version=$(node --print 'process.version.slice(1)')" >> "$GITHUB_OUTPUT" - echo "ubuntu_version=$(node --print '(+process.version.slice(1).split(`.`).at(0)) > 16 ? `noble` : `bionic`')" >> "$GITHUB_OUTPUT" - name: Set up QEMU uses: docker/setup-qemu-action@v3 + with: + image: tonistiigi/binfmt@sha256:66e11bea77a5ea9d6f0fe79b57cd2b189b5d15b93a2bdb925be22949232e4e55 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + with: + version: v0.18.0 + driver-opts: image=moby/buildkit:v0.17.2 + + - name: Init builder + run: docker buildx create --name builder --bootstrap --use + + - name: Build base image + run: | + ARCH=${{ matrix.linux_arch == 'arm64' && 'arm64v8' || matrix.linux_arch }} + NODE_VERSION=${{ steps.get_nodejs_version.outputs.version }} + TAG=$ARCH-alpine-lib-base-node-$NODE_VERSION + docker --debug buildx build --progress=plain --no-cache \ + --platform linux/${{ matrix.linux_arch }} \ + --build-arg="ARCH=$ARCH" \ + --build-arg="NODE_VERSION=$NODE_VERSION" \ + --output type=oci,dest=alpine-node-base,platform-split=false \ + -f ./.github/docker/Dockerfile.musl \ + -t $ARCH-alpine-lib-base-node-$NODE_VERSION \ + . + + ls + # cat alpine-node-base | docker load + + - id: upload-base-image + name: Upload base image + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.linux_arch == 'arm64' && 'arm64v8' || matrix.linux_arch }}-alpine-base-node-${{ steps.get_nodejs_version.outputs.version }} + path: alpine-node-base + if-no-files-found: 'error' + retention-days: 30 + compression-level: 0 - - name: Run Buildx + - name: Build libmongocrypt run: | - docker buildx create --name builder --bootstrap --use - docker buildx build \ + ARCH=${{ matrix.linux_arch == 'arm64' && 'arm64v8' || matrix.linux_arch }} + NODE_VERSION=${{ steps.get_nodejs_version.outputs.version }} + TAG=$ARCH-alpine-libmongocrypt-base-node-$NODE_VERSION + docker --debug buildx build --progress=plain --no-cache \ --platform linux/${{ matrix.linux_arch }} \ - --build-arg="NODE_ARCH=${{ matrix.linux_arch == 'amd64' && 'x64' || matrix.linux_arch }}" \ - --build-arg="NODE_VERSION=${{ steps.get_nodejs_version.outputs.version }}" \ - --build-arg="UBUNTU_VERSION=${{ steps.get_nodejs_version.outputs.ubuntu_version }}" \ - --build-arg="RUN_TEST=true" \ - --output type=local,dest=./prebuilds,platform-split=false \ - -f ./.github/docker/Dockerfile.glibc \ + --build-arg="ARCH=$ARCH" \ + --build-arg="NODE_VERSION=$NODE_VERSION" \ + --output type=oci,dest=alpine-libmongocrypt-base,platform-split=false \ + -f ./.github/docker/AddLibmongocrypt.musl \ + -t $ARCH-alpine-libmongocrypt-node-$NODE_VERSION \ . + docker load --input alpine-libmongocrypt-base + + - id: upload-image-with-libmongocrypt + name: Upload libmongocrypt + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.linux_arch == 'arm64' && 'arm64v8' || matrix.linux_arch }}-alpine-libmongocrypt-node-${{ steps.get_nodejs_version.outputs.version }} + path: alpine-libmongocrypt-base + if-no-files-found: 'error' + retention-days: 30 + compression-level: 0 + + - name: Build Prebuilds + run: | + ARCH=${{ matrix.linux_arch == 'arm64' && 'arm64v8' || matrix.linux_arch }} + NODE_VERSION=${{ steps.get_nodejs_version.outputs.version }} + TAG=$ARCH-alpine-prebuilds-base-node-$NODE_VERSION + docker --debug buildx build --progress=plain --no-cache \ + --platform linux/${{ matrix.linux_arch }} \ + --build-arg="ARCH=$ARCH" \ + --build-arg="NODE_VERSION=$NODE_VERSION" \ + --output type=oci,dest=alpine-prebuilds-base,platform-split=false \ + -f ./.github/docker/Prebuilds.musl \ + -t $ARCH-alpine-prebuilds-node-$NODE_VERSION \ + . + + - id: upload-images-with-prebuilds + name: Upload libmongocrypt + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.linux_arch == 'arm64' && 'arm64v8' || matrix.linux_arch }}-alpine-prebuilds-node-${{ steps.get_nodejs_version.outputs.version }} + path: alpine-prebuilds-base + if-no-files-found: 'error' + retention-days: 30 + compression-level: 0 + + # - name: Test + # run: | + # docker load --input alpine-prebuilds-base + # ARCH=${{ matrix.linux_arch == 'arm64' && 'arm64v8' || matrix.linux_arch }} + # NODE_VERSION=${{ steps.get_nodejs_version.outputs.version }} + # TAG=$ARCH-alpine-prebuilds-base-node-$NODE_VERSION + # docker run --platform linux/${{ matrix.linux_arch }} $ARCH-alpine-prebuilds-node-$NODE_VERSION bash -C "npm test" + \ No newline at end of file diff --git a/README.md b/README.md index 66c7521..1b04ee8 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ Below are the platforms that are available as prebuilds on each github release. - s390x - arm64 - x64 +- Linux MUSL 1.1.20 + - arm64 + - x64 - MacOS universal binary - x64 - arm64 diff --git a/etc/docker.sh b/etc/docker.sh new file mode 100644 index 0000000..495eaf6 --- /dev/null +++ b/etc/docker.sh @@ -0,0 +1,73 @@ +#! /bin/bash + +# script to aid in local testing of linux platforms +# requires a running docker instance + +# s390x, arm64, amd64 for ubuntu +# amd64 or arm64v8 for alpine +LINUX_ARCH=amd64 + +# 16.20.1+, default 16.20.1 +NODE_VERSION=20.0.0 + +SCRIPT_DIR=$(dirname ${BASH_SOURCE:-$0}) +PROJECT_DIR=$SCRIPT_DIR/.. + +# set -o xtrace + +build_and_test_musl() { + docker buildx create --name builder --bootstrap --use + + BASE_TAG=$LINUX_ARCH-alpine-base-node-$NODE_VERSION + # docker --debug buildx build --progress=plain \ + # --platform linux/$LINUX_ARCH --output=type=oci,dest=alpine-lib-base \ + # --build-arg="ARCH=$LINUX_ARCH" \ + # --build-arg="NODE_VERSION=$NODE_VERSION" \ + # --build-arg="RUN_TEST=true" \ + # -f ./.github/docker/Dockerfile.musl -t $BASE_TAG \ + # . + + # cat alpine-lib-base | docker load + + # LIBMONGOCRYPT_TAG=$LINUX_ARCH-alpine-libmongocrypt-node-$NODE_VERSION + # docker --debug buildx build --progress=plain --pull=false \ + # --output=type=oci,dest=alpine-libmongocrypt \ + # --platform linux/$LINUX_ARCH \ + # --build-arg="ARCH=$LINUX_ARCH" \ + # --build-arg="NODE_VERSION=$NODE_VERSION" \ + # -f ./.github/docker/AddLibmongocrypt.dockerfile -t $LIBMONGOCRYPT_TAG \ + # . + + # cat alpine-libmongocrypt | docker load + + LIBMONGOCRYPT_TAG=$LINUX_ARCH-alpine-prebuilds-node-$NODE_VERSION + # docker --debug buildx build --progress=plain --pull=false \ + # --load \ + # --platform linux/$LINUX_ARCH \ + # --build-arg="ARCH=$LINUX_ARCH" \ + # --build-arg="NODE_VERSION=$NODE_VERSION" \ + # -f ./.github/docker/Prebuilds.dockerfile -t $LIBMONGOCRYPT_TAG \ + # . + + docker --debug run $LIBMONGOCRYPT_TAG bash -c "npm test" +} + +build_and_test_glibc() { + docker buildx create --name builder --bootstrap --use + + UBUNTU_VERSION=$(node --print 'Number(process.argv[1].split(`.`).at(0)) > 16 ? `noble` : `bionic`' $NODE_VERSION) + NODE_ARCH=$(node -p 'process.argv[1] === `amd64` && `x64` || process.argv[1]' $LINUX_ARCH) + echo $UBUNTU_VERSION + docker buildx build --progress=plain --no-cache \ + --platform linux/$LINUX_ARCH \ + --build-arg="NODE_ARCH=$NODE_ARCH" \ + --build-arg="NODE_VERSION=$NODE_VERSION" \ + --build-arg="UBUNTU_VERSION=$UBUNTU_VERSION" \ + --build-arg="RUN_TEST=true" \ + --output type=local,dest=./prebuilds,platform-split=false \ + -f ./.github/docker/Dockerfile.glibc \ + $PROJECT_DIR +} + +build_and_test_musl +# build_and_test_glibc diff --git a/test/unit/release.test.ts b/test/unit/release.test.ts index f82c473..f436acd 100644 --- a/test/unit/release.test.ts +++ b/test/unit/release.test.ts @@ -37,21 +37,20 @@ describe(`Release ${packFile}`, function () { } }); - let tarFileList; - beforeEach(() => { + const tarFileSet: Set = new Set(); + before(() => { expect(fs.existsSync(packFile)).to.equal(false); cp.execSync('npm pack', { stdio: 'ignore' }); - tarFileList = []; tar.list({ file: packFile, sync: true, onentry(entry) { - tarFileList.push(entry.path); + tarFileSet.add(entry.path); } }); }); - afterEach(() => { + after(() => { if (process.arch === 'x64') { fs.unlinkSync(packFile); } @@ -59,12 +58,12 @@ describe(`Release ${packFile}`, function () { for (const requiredFile of REQUIRED_FILES) { it(`should contain ${requiredFile}`, () => { - expect(tarFileList).to.includes(requiredFile); + expect(tarFileSet).to.include(requiredFile); }); } it('should not have extraneous files', () => { - const unexpectedFileList = tarFileList.filter(f => !REQUIRED_FILES.some(r => r === f)); + const unexpectedFileList = Array.from(tarFileSet).filter(f => !REQUIRED_FILES.some(r => r === f)); expect(unexpectedFileList).to.have.lengthOf(0, `Extra files: ${unexpectedFileList.join(', ')}`); }); });