diff --git a/README.md b/README.md index 1bdbdbf..ff7e3da 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,25 @@ This project uses `esbuild` to compile the JavaScript source code for each actio ## Available Actions 1. **`build-docker-image`** - * Builds a Docker image, with optional caching and pushing via Azure Container Registry. + * Builds a Docker image, with optional caching and pushing via Azure Container Registry. This Action is Linux only. * See: [`actions/build-docker-image/action.yml`](./actions/build-docker-image/action.yml) - * *(Optional: Add link to a more detailed README if it exists at `actions/build-docker-image/README.md`)* + * See details: [`.github/actions/build-docker-image/README.md`](./.github/actions/build-docker-image/README.md) 2. **`run-build-script-in-docker`** - * Runs the ONNX Runtime `tools/ci_build/build.py` script inside a specified Docker container. + * Runs the ONNX Runtime `tools/ci_build/build.py` script inside a specified Docker container. This Action is Linux only. * Supports different modes (`update`, `build`, `test`). * Includes auto-detection for NVIDIA GPUs (`--gpus all`). * Manages common volume mounts (workspace, cache, test data). * Handles enabling execution providers via `--use_` flags. * See: [`actions/run-build-script-in-docker/action.yml`](./actions/run-build-script-in-docker/action.yml) +3. **`setup-vcpkg`** + * Downloads, verifies, bootstraps, and caches a specific tagged version of vcpkg. This Action is Windows only. + * Sets the `VCPKG_INSTALLATION_ROOT` environment variable for subsequent steps. + * Leverages `@actions/tool-cache` for efficient caching. + * See: [`.github/actions/setup-vcpkg/action.yml`](./.github/actions/setup-vcpkg/action.yml) + * See details: [`.github/actions/setup-vcpkg/README.md`](./.github/actions/setup-vcpkg/README.md) + ## Usage (for Consumers) Because the compiled action code (in the `build/` directory) is not present on the `main` branch or directly associated with version tags in the repository filesystem, you **cannot** use the actions directly like this: diff --git a/actions/setup-vcpkg/README.md b/actions/setup-vcpkg/README.md new file mode 100644 index 0000000..ebe4929 --- /dev/null +++ b/actions/setup-vcpkg/README.md @@ -0,0 +1,73 @@ +# Setup vcpkg GitHub Action + +This action downloads a specific tagged release archive of vcpkg from an internal mirror, verifies its SHA512 hash for integrity, extracts it, runs the necessary vcpkg bootstrap script (`bootstrap-vcpkg.bat` or `bootstrap-vcpkg.sh`), and caches the resulting installation using `@actions/tool-cache`. Currently this action only runs on Windows. + +It simplifies the process of getting a ready-to-use vcpkg instance in your GitHub Actions workflows, ensuring consistency and leveraging caching for speed. + +Once set up, it exports the `VCPKG_INSTALLATION_ROOT` environment variable pointing to the cached installation path, which can be used by subsequent steps (e.g., CMake toolchain configuration). ONNX Runtime's [build.py](https://github.com/microsoft/onnxruntime/blob/main/tools/ci_build/build.py) reads this environment variable. + +## Inputs + +| Input | Description | Required | Default | Example | +| -------------------- | ---------------------------------------------------------------- | :------: | --------------------------------------------- | -------------- | +| `vcpkg-version` | The vcpkg tag version to download (e.g., `2023.10.19`) | `true` | - | `2025.03.19` | +| `vcpkg-hash` | The expected SHA512 hash (used by Terrapin tool via `-s`). | `true` | - | `17e96169...` | +| `terrapin-tool-path` | Path to the `TerrapinRetrievalTool.exe` executable. | `true` | `C:/local/Terrapin/TerrapinRetrievalTool.exe` | | + +**Finding Hashes:** You typically need to download the specific tag's zip archive (e.g., `https://github.com/microsoft/vcpkg/archive/refs/tags/YYYY.MM.DD.zip`) and calculate its SHA512 hash locally. + +## Outputs + +| Output | Description | Example Usage | +| ------------ | ---------------------------------------------------- | ------------------------------------------ | +| `vcpkg-root` | The absolute path to the cached vcpkg installation. | `${{ steps.setup_vcpkg.outputs.vcpkg-root }}` | + +## Environment Variables + +This action sets the following environment variable for subsequent steps in the job: + +* **`VCPKG_INSTALLATION_ROOT`**: The absolute path to the cached vcpkg installation directory (same value as the `vcpkg-root` output). This is commonly used by build systems like CMake (`-DCMAKE_TOOLCHAIN_FILE=$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake`). + +## Caching + +* The action utilizes `@actions/tool-cache` for caching. +* The cache key is based on the tool name (`vcpkg`), the specified `vcpkg-version`, and the runner's architecture/OS. +* It caches the **bootstrapped** version of vcpkg, meaning the `vcpkg` executable should be present and ready to use within the cached directory. Subsequent runs hitting the cache will be significantly faster as they skip download, verification, extraction, and bootstrapping. + +## Example Usage + +```yaml +name: Build with vcpkg + +on: [push] + +jobs: + build: + runs-on: windows-latest # Or ubuntu-latest, macos-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup vcpkg + id: setup_vcpkg # Give it an ID to access outputs + # Assuming the action is in the same repository + uses: ./.github/actions/setup-vcpkg + with: + # Find specific versions and hashes on the vcpkg releases page or repo + vcpkg-version: '2025.03.19' # Replace with desired tag + vcpkg-hash: '17e96169cd3f266c4716fcdc1bb728e6a64f103941ece463a2834d50694eba4fb48f30135503fd466402afa139abc847ef630733c442595d1c34979f261b0114' # Replace with the correct SHA512 hash for the version + + - name: Use vcpkg (CMake Example) + shell: bash # Or pwsh, cmd + run: | + echo "Vcpkg is installed at: $VCPKG_INSTALLATION_ROOT" + echo "Vcpkg root output: ${{ steps.setup_vcpkg.outputs.vcpkg-root }}" + + # Example CMake configuration using the VCPKG_INSTALLATION_ROOT env var + cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=VCPKG\_INSTALLATION\_ROOT/scripts/buildsystems/vcpkg\.cmake +\# Or using the output directly \(less common for env vars\) +\# cmake \-B build \-S \. \-DCMAKE\_TOOLCHAIN\_FILE\={{ steps.setup_vcpkg.outputs.vcpkg-root }}/scripts/buildsystems/vcpkg.cmake + + cmake --build build + +``` \ No newline at end of file diff --git a/actions/setup-vcpkg/action.yml b/actions/setup-vcpkg/action.yml new file mode 100644 index 0000000..8a30977 --- /dev/null +++ b/actions/setup-vcpkg/action.yml @@ -0,0 +1,22 @@ +name: 'Setup vcpkg' +description: 'Downloads, verifies, extracts, and caches a specific tagged version of vcpkg' + +inputs: + vcpkg-version: + description: 'The vcpkg tag version to download (e.g., 2025.03.19)' + required: true + vcpkg-hash: + description: 'The expected SHA512 hash (used by Terrapin tool via -s)' + required: true + terrapin-tool-path: + description: 'Path to the TerrapinRetrievalTool.exe executable' + required: false + default: 'C:/local/Terrapin/TerrapinRetrievalTool.exe' + +outputs: + vcpkg-root: + description: 'The installation path of the cached vcpkg instance' + +runs: + using: 'node20' + main: 'dist/index.js' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a89200a..1c4e8cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,10 @@ "version": "0.0.2", "license": "MIT", "dependencies": { - "@actions/core": "^1.10.1", + "@actions/core": "^1.11.1", "@actions/exec": "^1.1.1", - "@actions/github": "^6.0.0" + "@actions/github": "^6.0.0", + "@actions/tool-cache": "^2.0.2" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", @@ -75,6 +76,19 @@ "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", "license": "MIT" }, + "node_modules/@actions/tool-cache": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-2.0.2.tgz", + "integrity": "sha512-fBhNNOWxuoLxztQebpOaWu6WeVmuwa77Z+DxIZ1B+OYvGkGQon6kTVg6Z32Cb13WCuw0szqonK+hh03mJV7Z6w==", + "license": "MIT", + "dependencies": { + "@actions/core": "^1.11.1", + "@actions/exec": "^1.0.0", + "@actions/http-client": "^2.0.1", + "@actions/io": "^1.1.1", + "semver": "^6.1.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -5826,7 +5840,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" diff --git a/package.json b/package.json index 7dd056e..68376a3 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,10 @@ "author": "", "license": "MIT", "dependencies": { - "@actions/core": "^1.10.1", + "@actions/core": "^1.11.1", "@actions/exec": "^1.1.1", - "@actions/github": "^6.0.0" + "@actions/github": "^6.0.0", + "@actions/tool-cache": "^2.0.2" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", diff --git a/src/setup-vcpkg/index.js b/src/setup-vcpkg/index.js new file mode 100644 index 0000000..9d1d899 --- /dev/null +++ b/src/setup-vcpkg/index.js @@ -0,0 +1,158 @@ +const core = require('@actions/core'); +const tc = require('@actions/tool-cache'); +const exec = require('@actions/exec'); +const crypto = require('node:crypto'); +const path = require('node:path'); +const fs = require('node:fs'); + +/** + * Verifies the SHA512 hash of a downloaded file. + * @param {string} filePath - Path to the file to verify. + * @param {string} expectedHash - The expected SHA512 hash in hex format. + * @returns {Promise} - True if the hash matches, false otherwise. + */ +async function verifySHA512(filePath, expectedHash) { + return new Promise((resolve, reject) => { + core.info(`Calculating SHA512 for file: ${filePath}`); + const hash = crypto.createHash('sha512'); + const stream = fs.createReadStream(filePath); + stream.on('error', err => reject(err)); + stream.on('data', chunk => hash.update(chunk)); + stream.on('end', () => { + const actualHash = hash.digest('hex'); + core.info(`Actual SHA512: ${actualHash}`); + core.info(`Expected SHA512: ${expectedHash}`); + resolve(actualHash.toLowerCase() === expectedHash.toLowerCase()); + }); + }); +} + +async function run() { + try { + // --- Get Inputs --- + const vcpkgVersion = core.getInput('vcpkg-version', { required: true }); + const vcpkgHash = core.getInput('vcpkg-hash', { required: true }); + const terrapinPath = core.getInput('terrapin-tool-path'); + + const toolName = 'vcpkg'; + const extractedFolderName = `vcpkg-${vcpkgVersion}`; + + core.info(`Setting up vcpkg version: ${vcpkgVersion}`); + core.info(`Using Terrapin Tool at: ${terrapinPath}`); + + // --- Cache Check --- + let vcpkgPath = tc.find(toolName, vcpkgVersion); + + if (vcpkgPath) { + core.info(`Found cached vcpkg at: ${vcpkgPath}`); + } else { + // --- Cache Miss --- + core.info(`vcpkg version ${vcpkgVersion} not found in cache. Downloading via Terrapin Tool...`); + + // --- Determine Download Destination --- + const runnerTempDir = process.env.RUNNER_TEMP; + if (!runnerTempDir) { + throw new Error('RUNNER_TEMP environment variable is not defined.'); + } + const vcpkgZipPath = path.join(runnerTempDir, 'vcpkg.zip'); + core.info(`Terrapin will download to: ${vcpkgZipPath}`); + + // --- Execute Terrapin Download --- + // Note: Terrapin might *also* verify the hash via -s argument. + const terrapinBaseUrl = 'https://vcpkg.storage.devpackages.microsoft.io/artifacts/'; + const terrapinPackageUrl = `https://github.com/microsoft/vcpkg/archive/refs/tags/${vcpkgVersion}.zip`; + const terrapinArgs = [ + '-b', terrapinBaseUrl, + '-a', 'true', + '-u', 'Environment', + '-p', terrapinPackageUrl, + '-s', vcpkgHash, // Hash for verification by Terrapin + '-d', vcpkgZipPath + ]; + + await core.group('Downloading vcpkg via Terrapin Tool', async () => { + await exec.exec(`"${terrapinPath}"`, terrapinArgs); + }); + core.info(`Download via Terrapin completed.`); + + // --- Verify Download Happened --- + if (!fs.existsSync(vcpkgZipPath)) { + throw new Error(`Download failed: Expected file not found at ${vcpkgZipPath} after Terrapin execution.`); + } + + // --- Verify SHA512 Hash (Manual Check) --- + core.info('Verifying SHA512 hash (manual check)...'); + const hashMatch = await verifySHA512(vcpkgZipPath, vcpkgHash); + if (!hashMatch) { + // Use core.setFailed for clear failure indication in GitHub Actions UI + core.setFailed('SHA512 hash verification failed! Downloaded file hash does not match expected hash.'); + return; // Stop execution if hash fails + } + core.info('Manual hash verification successful.'); + // --- End Manual Hash Check --- + + + // --- Extract --- + core.info(`Extracting ${vcpkgZipPath}...`); + const tempExtractPath = await tc.extractZip(vcpkgZipPath); + core.info(`Extracted vcpkg to temporary location: ${tempExtractPath}`); + + let extractedPath = path.join(tempExtractPath, extractedFolderName); + if (!fs.existsSync(extractedPath)) { + const files = fs.readdirSync(tempExtractPath); + core.warning(`Expected folder '${extractedFolderName}' not found directly. Contents: ${files.join(', ')}`); + if (files.length === 1 && fs.statSync(path.join(tempExtractPath, files[0])).isDirectory()) { + core.warning(`Assuming first directory '${files[0]}' is the correct one.`); + extractedPath = path.join(tempExtractPath, files[0]); + } else { + throw new Error(`Could not find the extracted vcpkg directory inside ${tempExtractPath}. Expected name pattern: ${extractedFolderName}`); + } + } + + // --- Bootstrap vcpkg --- + core.info(`Bootstrapping vcpkg in ${extractedPath}...`); + const bootstrapScriptName = process.platform === 'win32' ? 'bootstrap-vcpkg.bat' : 'bootstrap-vcpkg.sh'; + const bootstrapScriptPath = path.join(extractedPath, bootstrapScriptName); + const bootstrapArgs = ['-disableMetrics']; + + if (!fs.existsSync(bootstrapScriptPath)) { + throw new Error(`Bootstrap script not found at ${bootstrapScriptPath}`); + } + + await core.group('Running vcpkg bootstrap', async () => { + const options = { cwd: extractedPath }; + if (process.platform !== 'win32') { + await exec.exec('chmod', ['+x', bootstrapScriptPath]); + } + core.info(`Executing: ${bootstrapScriptPath} ${bootstrapArgs.join(' ')}`); + await exec.exec(bootstrapScriptPath, bootstrapArgs, options); + }); + core.info('vcpkg bootstrapped successfully.'); + + // --- Cache Directory --- + core.info(`Caching bootstrapped directory: ${extractedPath}`); + vcpkgPath = await tc.cacheDir(extractedPath, toolName, vcpkgVersion); + core.info(`Successfully cached vcpkg to: ${vcpkgPath}`); + } + + // --- Set Environment Variable & Output --- + core.info(`Setting VCPKG_INSTALLATION_ROOT to ${vcpkgPath}`); + core.exportVariable('VCPKG_INSTALLATION_ROOT', vcpkgPath); + core.setOutput('vcpkg-root', vcpkgPath); + + core.info('vcpkg setup complete.'); + + } catch (error) { + // Catch errors from exec, hash check failure (via setFailed), etc. + // If core.setFailed was already called, this will likely just reiterate. + // If an exception was thrown (e.g., download failed, file not found), this catches it. + core.setFailed(error.message); + } +} + +if (require.main === module) { + run(); +} + +// Always export run for testing or other programmatic usage +module.exports = { run }; \ No newline at end of file