├── .editorconfig ├── .gitignore ├── LICENSE.txt ├── README.md ├── action.yml ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── packed └── index.js ├── src ├── constants.ts ├── main.ts ├── msvc.ts ├── platform.ts ├── pm.ts ├── repo.ts ├── util.test.ts ├── util.ts ├── version.test.ts └── version.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{json, yml, js, ts}] 4 | indent_size = 2 5 | indent_style = space 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | start/build 2 | node_modules 3 | dist 4 | build 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # setup-sdl 2 | 3 | This GitHub action downloads, builds and installs SDL and its satellite libraries from source. 4 | 5 | By caching the result, subsequent workflow runs will be fast(er). 6 | 7 | ## Usage 8 | 9 | ```yaml 10 | name: "sdl" 11 | on: [push] 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: libsdl-org/setup-sdl@main 18 | id: sdl 19 | with: 20 | install-linux-dependencies: true 21 | version: 2-latest 22 | version-sdl-image: 2-latest 23 | 24 | - name: 'Configure and build your project' 25 | run: | 26 | cmake -S . -B build 27 | cmake --build build --verbose 28 | ``` 29 | 30 | ## CMake 31 | 32 | This action will build SDL using the default c/c++ toolchain, with CMake configuring the build. 33 | 34 | An alternative build toolchain can be configured by using a [CMake toolchain file](https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html). 35 | 36 | ## versions 37 | 38 | Using the `version` option, a SDL release can be used or the latest git tag: 39 | - `2-latest`: use the latest SDL2 release 40 | - `x.y.z`: use exactly a SDL `x.y.z` release (example: `2.8.1`) 41 | - `2-head`: use the latest SDL2 development commit 42 | - `3-latest`: use the latest SDL3 release 43 | - `3-head`: use the latest SDL3 development commit 44 | - ``: use an exact SDL git hash (repo: https://github.com/libsdl-org/SDL.git) 45 | 46 | Using the `version-sdl-*` inputs, it is possible to install the satellite libraries. 47 | They accept the same kind of input as `version`. 48 | 49 | ## Options 50 | 51 | See [action.yml](action.yml) for an overview of all options, and its defaults. 52 | 53 | ## FAQ 54 | 55 | ### My CMake project does not find SDL 56 | 57 | First, make sure you're looking for SDL using [`find_package`](https://cmake.org/cmake/help/latest/command/find_package.html): 58 | ```cmake 59 | # SDL2 60 | find_package(SDL2 REQUIRED CONFIG) 61 | 62 | # SDL3 63 | find_package(SDL3 REQUIRED CONFIG) 64 | ``` 65 | If CMake is still not able to find SDL, the minimum required CMake version of your project is probably less than 3.12. 66 | Since this version, CMake will also look for packages using environment variables as hints (see [CMP0074](https://cmake.org/cmake/help/latest/policy/CMP0074.html)). 67 | 68 | When bumping the minimum required CMake version is not desirable, here are 3 alternative methods (pick one!): 69 | - Add `-DCMAKE_PREFIX_PATH=${{ steps.sdl.outputs.prefix }}` to the CMake configure command (or add SDL's prefix to an already-existing `-DCMAKE_PREFIX_PATH=` argument) 70 | - Add `-DCMAKE_POLICY_DEFAULT_CMP0074=NEW` to the CMake configure command (this only works when the actual CMake version is >= 3.12). 71 | - Append `...3.12` to the minimum CMake version - this enables the new behaviour for policies introduced in CMake 3.12 or earlier while preserving support for older versions. 72 | 73 | ### `install-linux-dependencies` does things on non-Linux GitHub runners 74 | 75 | This input will be renamed to `install-dependencies`. 76 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup SDL" 2 | description: "Set up SDL" 3 | inputs: 4 | version: 5 | description: "Required version of SDL (2.x.y, 2-any, 2-latest, 2-head, 3-head), or git hash" 6 | default: "2-any" 7 | required: true 8 | version-sdl-image: 9 | description: "Version of SDL_image (2.x.y, 2-any, 2-latest, 2-head, 3-head), git hash, or " 10 | required: false 11 | version-sdl-mixer: 12 | description: "Version of SDL_mixer (2.x.y, 2-any, 2-latest, 2-head, 3-head), git hash, or " 13 | required: false 14 | version-sdl-net: 15 | description: "Version of SDL_net (2.x.y, 2-any, 2-latest, 2-head, 3-head), git hash, or " 16 | required: false 17 | version-sdl-rtf: 18 | description: "Version of SDL_rtf (2.x.y, 2-any, 2-latest, 2-head, 3-head), git hash, or " 19 | required: false 20 | version-sdl-ttf: 21 | description: "Version of SDL_ttf (2.x.y, 2-any, 2-latest, 2-head, 3-head), git hash, or " 22 | required: false 23 | version-sdl2-compat: 24 | description: "Version of sdl2-compat (2.x.y, 2-any, 2-latest, 2-head), git hash, or " 25 | required: false 26 | version-sdl12-compat: 27 | description: "Version of sdl12-compat (1.x.y, 1-any, 1-latest, 1-head), git hash, or " 28 | required: false 29 | pre-release: 30 | description: "Allow pre-releases" 31 | default: "true" 32 | required: true 33 | build-type: 34 | description: "CMake build type (Release/Debug/RelWithDebInfo/MinSizeRel)" 35 | default: "Debug" 36 | required: true 37 | sdl-test: 38 | description: "Build and install SDL_test library" 39 | default: "false" 40 | cmake-toolchain-file: 41 | description: "Path of a CMake toolchain file" 42 | cmake-generator: 43 | description: "CMake generator name (see https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html)" 44 | cmake-arguments: 45 | description: "Extra CMake arguments during configuration" 46 | install-linux-dependencies: 47 | description: "Install dependencies (supports apt-get, dnf, brew, and msys2-pacman)" 48 | shell: 49 | description: "Run commands as `$shell $file` (with $file containing the build commands)" 50 | discriminator: 51 | description: "Unique string to include in the GitHub SDL cache hash" 52 | add-to-environment: 53 | description: "Add path of the SDL (shared) library to PATH/LD_LIBRARY_PATH/DYLD_LIBRARY_PATH" 54 | default: "false" 55 | token: 56 | description: "A GitHub API token." 57 | default: ${{ github.token }} 58 | outputs: 59 | prefix: 60 | description: "Actual root of the built SDL package" 61 | version: 62 | description: "SDL version" 63 | runs: 64 | using: "node20" 65 | main: "packed/index.js" 66 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | 6 | export default tseslint.config( 7 | eslint.configs.recommended, 8 | tseslint.configs.recommended, 9 | ); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setup-sdl", 3 | "description": "GitHub actin to setup SDL", 4 | "license": "MIT", 5 | "homepage": "https://github.com/madebr/setup-sdl", 6 | "bugs": { 7 | "url": "https://github.com/madebr/setup-sdl/issues" 8 | }, 9 | "main": "dist/index.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/madebr/setup-sdl.git" 13 | }, 14 | "directories": { 15 | "test": "tests" 16 | }, 17 | "scripts": { 18 | "build": "tsc", 19 | "format": "prettier --write **/*.ts", 20 | "format-check": "prettier --check **/*.ts", 21 | "lint": "eslint src/*.ts", 22 | "pack": "ncc build build/main.js -o packed", 23 | "dev": "npm run build && npm run pack && node packed/index.js", 24 | "all": "npm run format && npm run lint && npm run build && npm run pack", 25 | "test": "jest" 26 | }, 27 | "jest": { 28 | "testEnvironment": "node", 29 | "testMatch": [ 30 | "**/*.test.ts" 31 | ], 32 | "transform": { 33 | "^.+\\.ts$": "ts-jest" 34 | } 35 | }, 36 | "keywords": [ 37 | "GitHub", 38 | "Actions", 39 | "SDL" 40 | ], 41 | "dependencies": { 42 | "@actions/cache": "^4.0", 43 | "@actions/core": "^1.10.1", 44 | "@actions/tool-cache": "^2.0.1", 45 | "@octokit/rest": "^21.0.1", 46 | "adm-zip": "^0.5.15", 47 | "octokit": "^4.1.3", 48 | "shlex": "^2.1.2", 49 | "uuid": "^11.1.0" 50 | }, 51 | "devDependencies": { 52 | "@types/adm-zip": "^0.5.5", 53 | "@types/jest": "^29.5.12", 54 | "@types/node": "^22.14.1", 55 | "@typescript-eslint/eslint-plugin": "^8.30.1", 56 | "@typescript-eslint/parser": "^8.30.1", 57 | "@vercel/ncc": "^0.38.1", 58 | "eslint": "^9.25.0", 59 | "eslint-plugin-github": "^6.0.0", 60 | "jest": "^29.7.0", 61 | "js-yaml": "^4.1.0", 62 | "prettier": "3.5.3", 63 | "ts-jest": "^29.2.4", 64 | "typescript": "^5.5.4" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const NINJA_VERSION = "1.11.1"; 2 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process"; 2 | import * as crypto from "crypto"; 3 | import * as fs from "fs"; 4 | import * as os from "os"; 5 | import * as path from "path"; 6 | import { createWriteStream } from "node:fs"; 7 | import { pipeline } from "node:stream/promises"; 8 | 9 | import * as cache from "@actions/cache"; 10 | import * as core from "@actions/core"; 11 | import { Octokit } from "@octokit/rest"; 12 | import AdmZip from "adm-zip"; 13 | 14 | import { convert_git_branch_tag_to_hash } from "./repo"; 15 | import { SetupSdlError, command_arglist_to_string, shlex_split } from "./util"; 16 | import * as pm from "./pm"; 17 | 18 | import { GitHubRelease, ReleaseDb, ReleaseType } from "./version"; 19 | 20 | import { 21 | export_environment_variables, 22 | get_sdl_build_platform, 23 | get_platform_root_directory, 24 | SdlBuildPlatform, 25 | } from "./platform"; 26 | 27 | interface GitSubmodule { 28 | path: string; 29 | repo_owner: string; 30 | repo_name: string; 31 | branch: string; 32 | } 33 | 34 | function read_gitmodules(path: string): GitSubmodule[] { 35 | if (!fs.existsSync(path)) { 36 | return []; 37 | } 38 | const submodules = []; 39 | const sdl_repo_regex = 40 | /https:\/\/github\.com\/([a-zA-Z0-9_-]+)\/([0-9a-zA-Z_-]+)(\.git)?/; 41 | const gitmodules_lines = fs 42 | .readFileSync(path, { encoding: "utf8" }) 43 | .trim() 44 | .split("\n"); 45 | for (let i = 0; 4 * i + 3 <= gitmodules_lines.length; i += 1) { 46 | const path = gitmodules_lines[4 * i + 1].split("=")[1].trim(); 47 | const url = gitmodules_lines[4 * i + 2].split("=")[1].trim(); 48 | const match = url.match(sdl_repo_regex); 49 | if (!match) { 50 | throw new SetupSdlError(`Unable to extract owner/name from "${url}"`); 51 | } 52 | const repo_owner = match[1]; 53 | const repo_name = match[2]; 54 | const branch = gitmodules_lines[4 * i + 3].split("=")[1].trim(); 55 | submodules.push({ 56 | path: path, 57 | repo_owner: repo_owner, 58 | repo_name: repo_name, 59 | branch: branch, 60 | }); 61 | } 62 | return submodules; 63 | } 64 | 65 | async function download_git_repo(args: { 66 | repo_owner: string; 67 | repo_name: string; 68 | submodules: boolean; 69 | git_hash: string; 70 | directory: string; 71 | octokit: Octokit; 72 | }) { 73 | fs.mkdirSync(args.directory, { recursive: true }); 74 | await core.group( 75 | `Downloading and extracting ${args.repo_owner}/${args.repo_name} (${args.git_hash}) into ${args.directory}`, 76 | async () => { 77 | core.info("Downloading git zip archive..."); 78 | /* Use streams to avoid HTTP 500 HttpError/RequestError: other side closed 79 | * https://github.com/octokit/rest.js/issues/12#issuecomment-1916023479 80 | * https://github.com/octokit/rest.js/issues/461#issuecomment-2293930969 81 | */ 82 | const response = await args.octokit.rest.repos.downloadZipballArchive({ 83 | owner: args.repo_owner, 84 | repo: args.repo_name, 85 | ref: args.git_hash, 86 | request: { 87 | parseSuccessResponseBody: false, // required to access response as stream 88 | }, 89 | }); 90 | const assetStream = response.data as unknown as NodeJS.ReadableStream; 91 | const ARCHIVE_PATH = path.join(args.directory, "archive.zip"); 92 | const outputFile = createWriteStream(ARCHIVE_PATH); 93 | core.info("Writing zip archive to disk..."); 94 | await pipeline(assetStream, outputFile); 95 | 96 | core.info("Extracting zip archive..."); 97 | const admzip = new AdmZip(ARCHIVE_PATH); 98 | admzip.getEntries().forEach((entry) => { 99 | if (entry.isDirectory) { 100 | /* Ignore directories */ 101 | } else { 102 | const pos_first_slash = entry.entryName.indexOf("/"); 103 | const pos_last_slash = entry.entryName.lastIndexOf("/"); 104 | const targetPath = path.join( 105 | args.directory, 106 | entry.entryName.slice(pos_first_slash + 1, pos_last_slash), 107 | ); 108 | const maintainEntryPath = true; 109 | const overwrite = false; 110 | const keepOriginalPermission = false; 111 | const outFileName = entry.entryName.slice(pos_last_slash + 1); 112 | core.debug( 113 | `Extracting ${outFileName} to ${path.join( 114 | targetPath, 115 | outFileName, 116 | )}...`, 117 | ); 118 | admzip.extractEntryTo( 119 | entry, 120 | targetPath, 121 | maintainEntryPath, 122 | overwrite, 123 | keepOriginalPermission, 124 | outFileName, 125 | ); 126 | } 127 | }); 128 | }, 129 | ); 130 | if (args.submodules) { 131 | const submodules = read_gitmodules(`${args.directory}/.gitmodules`); 132 | for (const submodule of submodules) { 133 | const submodule_hash = await convert_git_branch_tag_to_hash({ 134 | branch_or_hash: submodule.branch, 135 | owner: submodule.repo_owner, 136 | repo: submodule.repo_name, 137 | octokit: args.octokit, 138 | }); 139 | const submodule_directory = `${args.directory}/${submodule.path}`; 140 | await download_git_repo({ 141 | repo_owner: submodule.repo_owner, 142 | repo_name: submodule.repo_name, 143 | submodules: false, 144 | git_hash: submodule_hash, 145 | directory: submodule_directory, 146 | octokit: args.octokit, 147 | }); 148 | } 149 | } 150 | } 151 | 152 | export class Executor { 153 | shell?: string | undefined; 154 | 155 | constructor(args: { shell: string | undefined }) { 156 | this.shell = args.shell; 157 | } 158 | 159 | run(command: string, stdio_inherit: boolean = false) { 160 | core.info(`${command}`); 161 | let final_command: string; 162 | if (this.shell && this.shell.indexOf("{0}") >= 0) { 163 | const cmd_file = `${os.tmpdir}/cmd.txt`; 164 | fs.writeFileSync(cmd_file, command); 165 | final_command = this.shell.replace("{0}", cmd_file); 166 | core.info(`-> ${final_command}`); 167 | } else { 168 | final_command = command; 169 | } 170 | const stdio_options: { stdio?: "inherit" } = {}; 171 | if (stdio_inherit) { 172 | stdio_options.stdio = "inherit"; 173 | } 174 | child_process.execSync(final_command, stdio_options); 175 | } 176 | } 177 | 178 | async function cmake_configure_build(args: { 179 | project: string; 180 | source_dir: string; 181 | build_dir: string; 182 | package_dir: string; 183 | build_type: string; 184 | cmake_configure_args: string[]; 185 | executor: Executor; 186 | }) { 187 | const configure_args = [ 188 | "cmake", 189 | "-S", 190 | args.source_dir, 191 | "-B", 192 | args.build_dir, 193 | '-DSDL_VENDOR_INFO="libsdl-org/setup-sdl"', 194 | "-DSDL_CMAKE_DEBUG_POSTFIX=", 195 | ...args.cmake_configure_args, 196 | `-DCMAKE_INSTALL_PREFIX=${args.package_dir}`, 197 | ]; 198 | if (core.isDebug()) { 199 | configure_args.push("--trace-expand"); 200 | } 201 | 202 | const build_args = [ 203 | "cmake", 204 | "--build", 205 | args.build_dir, 206 | "--config", 207 | args.build_type, 208 | "--verbose", 209 | ]; 210 | 211 | const install_args = [ 212 | "cmake", 213 | "--install", 214 | args.build_dir, 215 | "--config", 216 | args.build_type, 217 | ]; 218 | 219 | await core.group(`Configuring ${args.project} (CMake)`, async () => { 220 | core.debug(`configure_args: ${configure_args}`); 221 | const configure_command = command_arglist_to_string(configure_args); 222 | core.debug(`configure_command: ${configure_command}`); 223 | args.executor.run(configure_command, true); 224 | }); 225 | await core.group(`Building ${args.project} (CMake)`, async () => { 226 | core.debug(`build_args: ${build_args}`); 227 | const build_command = command_arglist_to_string(build_args); 228 | core.debug(`build_command: ${build_command}`); 229 | args.executor.run(build_command, true); 230 | }); 231 | await core.group(`Installing ${args.project} (CMake)`, async () => { 232 | core.debug(`install_args: ${install_args}`); 233 | const install_command = command_arglist_to_string(install_args); 234 | core.debug(`install_command: ${install_command}`); 235 | args.executor.run(install_command, true); 236 | }); 237 | } 238 | 239 | function calculate_state_hash(args: { 240 | git_hash: string; 241 | build_platform: SdlBuildPlatform; 242 | executor: Executor; 243 | cmake_toolchain_file: string | undefined; 244 | cmake_configure_arguments: string | undefined; 245 | package_manager: pm.PackageManagerType | undefined; 246 | dependency_hashes: { [_: string]: string }; 247 | }) { 248 | const ENV_KEYS = [ 249 | "AR", 250 | "CC", 251 | "CXX", 252 | "ARFLAGS", 253 | "CFLAGS", 254 | "CXXFLAGS", 255 | "INCLUDES", 256 | "LDFLAGS", 257 | "LIB", 258 | "LIBPATH", 259 | "MSYSTEM", 260 | "PKG_CONFIG_PATH", 261 | ]; 262 | const env_state: string[] = []; 263 | for (const key of ENV_KEYS) { 264 | env_state.push(`${key}=${process.env[key]}`); 265 | } 266 | for (const key of Object.keys(process.env)) { 267 | if (key.startsWith("CMAKE_")) { 268 | env_state.push(`${key}=${process.env[key]}`); 269 | } 270 | } 271 | 272 | const ACTION_KEYS = [ 273 | "build-type", 274 | "cmake-toolchain-file", 275 | "cmake-generator", 276 | "discriminator", 277 | "sdl-test", 278 | ]; 279 | const inputs_state: string[] = []; 280 | for (const key of ACTION_KEYS) { 281 | const v = core.getInput(key); 282 | inputs_state.push(`${key}=${v}`); 283 | } 284 | 285 | const misc_state = [ 286 | `GIT_HASH=${args.git_hash}`, 287 | `build_platform=${args.build_platform}`, 288 | `shell=${args.executor.shell}`, 289 | ]; 290 | 291 | if (args.package_manager) { 292 | misc_state.push(`package_manager=${args.package_manager}`); 293 | } 294 | 295 | if (args.cmake_toolchain_file) { 296 | const toolchain_contents = fs.readFileSync(args.cmake_toolchain_file, { 297 | encoding: "utf8", 298 | }); 299 | const cmake_toolchain_file_hash = crypto 300 | .createHash("sha256") 301 | .update(toolchain_contents) 302 | .digest("hex"); 303 | misc_state.push(`cmake_toolchain_file_hash=${cmake_toolchain_file_hash}`); 304 | } 305 | 306 | if (args.cmake_configure_arguments) { 307 | misc_state.push(`cmake_arguments=${args.cmake_configure_arguments}`); 308 | } 309 | 310 | for (const dep in args.dependency_hashes) { 311 | misc_state.push(`dependency_${dep}=${args.dependency_hashes[dep]}`); 312 | } 313 | 314 | const complete_state: string[] = [ 315 | "ENVIRONMENT", 316 | ...env_state, 317 | "INPUTS", 318 | ...inputs_state, 319 | "MISC", 320 | ...misc_state, 321 | ]; 322 | 323 | const state_string = complete_state.join("##"); 324 | 325 | core.debug(`state_string=${state_string}`); 326 | 327 | return crypto.createHash("sha256").update(state_string).digest("hex"); 328 | } 329 | 330 | function resolve_workspace_path(in_path: string): string | undefined { 331 | if (!in_path) { 332 | return undefined; 333 | } 334 | if (fs.existsSync(in_path)) { 335 | return path.resolve(in_path); 336 | } 337 | const workspace_path = path.resolve( 338 | `${process.env.GITHUB_WORKSPACE}`, 339 | in_path, 340 | ); 341 | if (fs.existsSync(workspace_path)) { 342 | return workspace_path; 343 | } 344 | return undefined; 345 | } 346 | 347 | function get_cmake_toolchain_path(): string | undefined { 348 | const in_cmake_toolchain_file = core.getInput("cmake-toolchain-file"); 349 | if (in_cmake_toolchain_file) { 350 | const resolved_cmake_toolchain_file = resolve_workspace_path( 351 | in_cmake_toolchain_file, 352 | ); 353 | if (!resolved_cmake_toolchain_file) { 354 | throw new SetupSdlError( 355 | `Cannot find CMake toolchain file: ${in_cmake_toolchain_file}`, 356 | ); 357 | } 358 | return resolved_cmake_toolchain_file; 359 | } 360 | const env_cmake_toolchain_file = process.env.CMAKE_TOOLCHAIN_FILE; 361 | if (env_cmake_toolchain_file) { 362 | const resolved_cmake_toolchain_file = resolve_workspace_path( 363 | env_cmake_toolchain_file, 364 | ); 365 | if (!resolved_cmake_toolchain_file) { 366 | throw new SetupSdlError( 367 | `Cannot find CMake toolchain file: ${env_cmake_toolchain_file}`, 368 | ); 369 | } 370 | return resolved_cmake_toolchain_file; 371 | } 372 | return undefined; 373 | } 374 | 375 | function parse_package_manager(args: { 376 | build_platform: SdlBuildPlatform; 377 | input: string | undefined; 378 | }): pm.PackageManagerType | undefined { 379 | if (!args.input) { 380 | return undefined; 381 | } 382 | const input = args.input.trim().toLowerCase(); 383 | if (input.length == 0) { 384 | return undefined; 385 | } 386 | if (input == "false") { 387 | return undefined; 388 | } else if (input == "true") { 389 | return pm.detect_package_manager({ build_platform: args.build_platform }); 390 | } else { 391 | return pm.package_manager_type_from_string(input); 392 | } 393 | } 394 | 395 | async function install_dependencies(args: { 396 | project: Project; 397 | package_manager_type: pm.PackageManagerType; 398 | packages: pm.Packages; 399 | executor: Executor; 400 | }) { 401 | const package_manager = pm.create_package_manager({ 402 | type: args.package_manager_type, 403 | executor: args.executor, 404 | }); 405 | const pm_packages = args.packages[args.package_manager_type]; 406 | if (pm_packages && !package_manager) { 407 | core.info( 408 | `Don't know how to install packages the for current platform (${args.package_manager_type}). Please create a pr.`, 409 | ); 410 | return; 411 | } 412 | if (!pm_packages) { 413 | return; 414 | } 415 | await core.group( 416 | `Installing ${args.project} dependencies using ${args.package_manager_type}`, 417 | async () => { 418 | package_manager.update(); 419 | package_manager.install(pm_packages.required); 420 | pm_packages.optional.forEach((optional_package) => { 421 | try { 422 | package_manager.install([optional_package]); 423 | } catch { 424 | /* intentionally left blank */ 425 | } 426 | }); 427 | }, 428 | ); 429 | } 430 | 431 | import { 432 | parse_version_string, 433 | Project, 434 | project_descriptions, 435 | ParsedVersion, 436 | VersionExtractor, 437 | Version, 438 | } from "./version"; 439 | 440 | async function run() { 441 | core.debug("hello"); 442 | const GITHUB_TOKEN = core.getInput("token"); 443 | process.env.GH_TOKEN = GITHUB_TOKEN; 444 | process.env.GITHUB_TOKEN = GITHUB_TOKEN; 445 | 446 | const OCTOKIT = new Octokit({ auth: GITHUB_TOKEN }); 447 | 448 | const SDL_BUILD_PLATFORM = get_sdl_build_platform(); 449 | core.info(`build platform = ${SDL_BUILD_PLATFORM}`); 450 | 451 | const SETUP_SDL_ROOT = get_platform_root_directory(SDL_BUILD_PLATFORM); 452 | core.info(`root = ${SETUP_SDL_ROOT}`); 453 | 454 | const SHELL = (() => { 455 | let shell_in = core.getInput("shell"); 456 | const IGNORED_SHELLS = ["bash", "cmd", "powershell", "pwsh", "sh"]; 457 | if (IGNORED_SHELLS.indexOf(shell_in) >= 0) { 458 | shell_in = ""; 459 | } 460 | return shell_in; 461 | })(); 462 | const EXECUTOR = new Executor({ shell: SHELL }); 463 | const ALLOW_PRE_RELEASE = core.getBooleanInput("pre-release"); 464 | const CMAKE_TOOLCHAIN_FILE = get_cmake_toolchain_path(); 465 | const CMAKE_BUILD_TYPE = core.getInput("build-type"); 466 | const CMAKE_BUILD_TYPES = [ 467 | "Release", 468 | "Debug", 469 | "MinSizeRel", 470 | "RelWithDebInfo", 471 | ]; 472 | if (!CMAKE_BUILD_TYPES.includes(CMAKE_BUILD_TYPE)) { 473 | throw new SetupSdlError("Invalid build-type"); 474 | } 475 | const PACKAGE_MANAGER_TYPE = parse_package_manager({ 476 | build_platform: SDL_BUILD_PLATFORM, 477 | input: core.getInput("install-linux-dependencies"), 478 | }); 479 | const BUILD_SDL_TEST = core.getBooleanInput("sdl-test"); 480 | 481 | let major_version: number | null = null; 482 | 483 | // Parse inputs 484 | const requested_versions: { [key in Project]?: ParsedVersion } = {}; 485 | for (const project in Project) { 486 | const description = project_descriptions[project as Project]; 487 | const version_string = core.getInput(description.option_name); 488 | core.debug(`Project ${project}: input "${version_string}"`); 489 | if (!version_string) { 490 | continue; 491 | } 492 | const parsed_version = parse_version_string( 493 | version_string, 494 | description.discarded_prefix, 495 | ); 496 | switch (parsed_version.type) { 497 | case ReleaseType.Any: 498 | case ReleaseType.Head: 499 | case ReleaseType.Latest: 500 | case ReleaseType.Exact: { 501 | const v = parsed_version.version as Version; 502 | if (major_version != null) { 503 | if (major_version != v.major) { 504 | throw new SetupSdlError( 505 | `Version(s) are incompatiable: all must have the same major version.`, 506 | ); 507 | } 508 | major_version = v.major; 509 | } 510 | break; 511 | } 512 | } 513 | 514 | core.debug( 515 | `Project ${project}: requested version type: ${ 516 | parsed_version.type 517 | }, version: ${parsed_version.version.toString()}`, 518 | ); 519 | requested_versions[project as Project] = parsed_version; 520 | } 521 | 522 | // Create build plan 523 | const build_order: Project[] = []; 524 | let projects_left: Project[] = Object.keys(requested_versions) as Project[]; 525 | 526 | while (projects_left.length > 0) { 527 | core.debug(`projects_left=${projects_left} order=${build_order}`); 528 | const new_projects_left: Project[] = []; 529 | for (const project_left of projects_left) { 530 | core.debug( 531 | `project: ${project_left} deps: ${project_descriptions[project_left].deps}`, 532 | ); 533 | if ( 534 | project_descriptions[project_left].deps.every((proj) => { 535 | core.debug(`proj=${proj}`); 536 | core.debug(`build_order=${build_order}`); 537 | core.debug(`proj in build_order=${proj in build_order}`); 538 | if ( 539 | proj == Project.SDL12_compat && 540 | (build_order.indexOf(Project.SDL) >= 0 || 541 | build_order.indexOf(Project.SDL2_compat) >= 0) 542 | ) { 543 | return 1; 544 | } 545 | if ( 546 | proj == Project.SDL2_compat && 547 | build_order.indexOf(Project.SDL) >= 0 548 | ) { 549 | return 1; 550 | } 551 | return build_order.findIndex((e) => e == proj) >= 0; 552 | }) 553 | ) { 554 | build_order.push(project_left); 555 | } else { 556 | new_projects_left.push(project_left); 557 | } 558 | } 559 | if (new_projects_left.length == projects_left.length) { 560 | throw new SetupSdlError(`Unable to establish build order`); 561 | } 562 | projects_left = new_projects_left; 563 | } 564 | core.info(`Build order is ${build_order}.`); 565 | 566 | // Execute build plan 567 | 568 | const project_hashes: { [key in Project]?: string } = {}; 569 | const package_dirs: { [key in Project]?: string } = {}; 570 | const project_versions: { [key in Project]?: Version } = {}; 571 | 572 | for (const project of build_order) { 573 | const project_description = project_descriptions[project]; 574 | const req_step_version = requested_versions[project]; 575 | 576 | // Calculate branch name 577 | const git_branch_hash: string = (() => { 578 | if (req_step_version == undefined) { 579 | if (major_version == undefined) { 580 | throw new SetupSdlError( 581 | `Don't know what branch/hash to fetch for ${project}.`, 582 | ); 583 | } 584 | const branch = project_description.version_branch_map[major_version]; 585 | if (branch == undefined) { 586 | throw new SetupSdlError( 587 | `Don't know what branch to use for ${project} ${major_version}.`, 588 | ); 589 | } 590 | return branch; 591 | } 592 | if (req_step_version.type == ReleaseType.Commit) { 593 | return req_step_version.version as string; 594 | } 595 | const req_version = req_step_version.version as Version; 596 | if (req_step_version.type == ReleaseType.Head) { 597 | const branch = 598 | project_description.version_branch_map[req_version.major]; 599 | if (branch == undefined) { 600 | throw new SetupSdlError("Invalid -head version"); 601 | } 602 | return branch; 603 | } 604 | const github_releases = GitHubRelease.fetch_all( 605 | `${project_description.repo_owner}/${project_description.repo_name}`, 606 | ); 607 | const release_db = ReleaseDb.create(github_releases); 608 | const sdl_release = release_db.find( 609 | req_version, 610 | ALLOW_PRE_RELEASE, 611 | req_step_version.type, 612 | ); 613 | if (!sdl_release) { 614 | throw new SetupSdlError( 615 | `Could not find a matching release for ${project} ${req_version}`, 616 | ); 617 | } 618 | return sdl_release.tag; 619 | })(); 620 | 621 | // Calculate git hash 622 | const git_hash: string = await convert_git_branch_tag_to_hash({ 623 | branch_or_hash: git_branch_hash, 624 | owner: project_description.repo_owner, 625 | repo: project_description.repo_name, 626 | octokit: OCTOKIT, 627 | }); 628 | const project_cmake_arguments = (() => { 629 | const args = []; 630 | const input_cmake_arguments = core.getInput("cmake-arguments"); 631 | if (input_cmake_arguments) { 632 | args.push(input_cmake_arguments); 633 | } 634 | if (project == Project.SDL) { 635 | args.push(`-DSDL_TEST_LIBRARY=${BUILD_SDL_TEST}`); 636 | args.push(`-DSDL_UNIX_CONSOLE_BUILD=ON`); 637 | } 638 | return args.join(" "); 639 | })(); 640 | 641 | // Calculate unique hash for caching 642 | const dependency_hashes = (() => { 643 | const dep_hashes: { [_: string]: string } = {}; 644 | for (const dep of project_description.deps) { 645 | dep_hashes[dep as string] = project_hashes[dep as Project] as string; 646 | } 647 | return dep_hashes; 648 | })(); 649 | 650 | const project_hash = calculate_state_hash({ 651 | git_hash: git_hash, 652 | build_platform: SDL_BUILD_PLATFORM, 653 | executor: EXECUTOR, 654 | cmake_toolchain_file: CMAKE_TOOLCHAIN_FILE, 655 | cmake_configure_arguments: project_cmake_arguments, 656 | package_manager: PACKAGE_MANAGER_TYPE, 657 | dependency_hashes: dependency_hashes, 658 | }); 659 | project_hashes[project] = project_hash; 660 | 661 | const package_dir = `${SETUP_SDL_ROOT}/${project_hash}/package`; 662 | package_dirs[project] = package_dir; 663 | 664 | const cache_key = `setup-sdl-${project}-${project_hash}`; 665 | const cache_paths = [package_dir]; 666 | 667 | // Look in cache 668 | const was_in_cache = await core.group( 669 | `Looking up a ${project} build in the cache`, 670 | async () => { 671 | core.info(`setup-sdl ${project} state = ${project_hash}`); 672 | 673 | // Pass a copy of cache_paths since cache.restoreCache modifies/modified its arguments 674 | const found_cache_key = await cache.restoreCache( 675 | cache_paths.slice(), 676 | cache_key, 677 | ); 678 | if (found_cache_key) { 679 | core.info(`${project} found in the cache: key = ${found_cache_key}`); 680 | } else { 681 | core.info( 682 | `No match found in cache. Building ${project} from scratch.`, 683 | ); 684 | } 685 | 686 | return !!found_cache_key; 687 | }, 688 | ); 689 | 690 | // Always install linux dependencies (SDL_ttf links to libfreetype) 691 | const project_packages = project_description.packages; 692 | if (project_packages && PACKAGE_MANAGER_TYPE) { 693 | await install_dependencies({ 694 | project: project, 695 | executor: EXECUTOR, 696 | package_manager_type: PACKAGE_MANAGER_TYPE, 697 | packages: project_packages, 698 | }); 699 | } 700 | 701 | // if not in cache, fetch sources + build + install + store 702 | if (!was_in_cache) { 703 | const source_dir = `${SETUP_SDL_ROOT}/${project_hash}/source`; 704 | const build_dir = `${SETUP_SDL_ROOT}/${project_hash}/build`; 705 | 706 | await download_git_repo({ 707 | repo_owner: project_description.repo_owner, 708 | repo_name: project_description.repo_name, 709 | submodules: true, 710 | git_hash: git_hash, 711 | directory: source_dir, 712 | octokit: OCTOKIT, 713 | }); 714 | 715 | const cmake_configure_args = shlex_split(project_cmake_arguments); 716 | 717 | cmake_configure_args.push( 718 | `-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}`, 719 | "-DCMAKE_INSTALL_BINDIR=bin", 720 | "-DCMAKE_INSTALL_INCLUDEDIR=include", 721 | "-DCMAKE_INSTALL_LIBDIR=lib", 722 | ); 723 | if (CMAKE_TOOLCHAIN_FILE) { 724 | cmake_configure_args.push( 725 | `-DCMAKE_TOOLCHAIN_FILE="${CMAKE_TOOLCHAIN_FILE}"`, 726 | ); 727 | } 728 | 729 | const CMAKE_GENERATOR = core.getInput("cmake-generator"); 730 | if (CMAKE_GENERATOR && CMAKE_GENERATOR.length > 0) { 731 | cmake_configure_args.push("-G", `"${CMAKE_GENERATOR}"`); 732 | } 733 | 734 | await cmake_configure_build({ 735 | project: project, 736 | source_dir: source_dir, 737 | build_dir: build_dir, 738 | package_dir: package_dir, 739 | build_type: CMAKE_BUILD_TYPE, 740 | cmake_configure_args: cmake_configure_args, 741 | executor: EXECUTOR, 742 | }); 743 | 744 | await core.group(`Storing ${project} in the cache`, async () => { 745 | core.info(`Caching ${cache_paths}.`); 746 | // Pass a copy of cache_paths since cache.saveCache modifies/modified its arguments 747 | await cache.saveCache(cache_paths.slice(), cache_key); 748 | }); 749 | } 750 | 751 | const version_extractor = new VersionExtractor(project_description); 752 | const project_version = 753 | version_extractor.extract_from_install_prefix(package_dir); 754 | project_versions[project] = project_version; 755 | core.info(`${project} version is ${project_version.toString()}`); 756 | 757 | // Set environment variable (e.g. SDL3_ROOT) 758 | const infix = project_version.major == 1 ? "" : `${project_version.major}`; 759 | const cmake_export_name = `${project_description.cmake_var_out_prefix}${infix}${project_description.cmake_var_out_suffix}`; 760 | core.exportVariable(cmake_export_name, package_dir); 761 | } 762 | 763 | if (core.getBooleanInput("add-to-environment")) { 764 | export_environment_variables( 765 | SDL_BUILD_PLATFORM, 766 | Object.values(package_dirs), 767 | ); 768 | } 769 | 770 | // Append /lib/pkgconfig to PKG_CONFIG_PATH 771 | const pkg_config_path = (() => { 772 | const extra_pkg_config_paths = Object.values(package_dirs).map( 773 | (package_dir) => { 774 | return `${package_dir}/lib/pkgconfig`; 775 | }, 776 | ); 777 | let pkg_config_path = process.env.PKG_CONFIG_PATH || ""; 778 | if (pkg_config_path) { 779 | pkg_config_path += path.delimiter; 780 | } 781 | pkg_config_path += extra_pkg_config_paths.join(path.delimiter); 782 | return pkg_config_path; 783 | })(); 784 | core.exportVariable("PKG_CONFIG_PATH", pkg_config_path); 785 | 786 | // Set SDL2_CONFIG environment variable 787 | if (major_version == 2) { 788 | const sdl2_config = [package_dirs[Project.SDL], "bin", "sdl2-config"].join( 789 | "/", 790 | ); 791 | core.exportVariable(`SDL2_CONFIG`, sdl2_config); 792 | } 793 | 794 | core.setOutput("prefix", package_dirs[Project.SDL] as string); 795 | core.setOutput( 796 | "version", 797 | (project_versions[Project.SDL] as Version).toString(), 798 | ); 799 | } 800 | 801 | try { 802 | run(); 803 | } catch (e) { 804 | if (e instanceof Error) { 805 | core.error(e.message); 806 | core.setFailed(e.message); 807 | } 808 | throw e; 809 | } 810 | -------------------------------------------------------------------------------- /src/msvc.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 ilammy 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | // this software and associated documentation files (the "Software"), to deal in 5 | // the Software without restriction, including without limitation the rights to 6 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | // the Software, and to permit persons to whom the Software is furnished to do so, 8 | // subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | import * as child_process from "child_process"; 21 | import * as fs from "fs"; 22 | import * as path from "path"; 23 | import * as process from "process"; 24 | 25 | import * as core from "@actions/core"; 26 | 27 | import { SetupSdlError } from "./util"; 28 | 29 | const PROGRAM_FILES_X86 = process.env["ProgramFiles(x86)"]; 30 | const PROGRAM_FILES = [ 31 | process.env["ProgramFiles(x86)"], 32 | process.env["ProgramFiles"], 33 | ]; 34 | 35 | const EDITIONS = ["Enterprise", "Professional", "Community"]; 36 | const YEARS = ["2022", "2019", "2017"]; 37 | 38 | const VsYearVersion: { [key: string]: string } = { 39 | "2022": "17.0", 40 | "2019": "16.0", 41 | "2017": "15.0", 42 | "2015": "14.0", 43 | "2013": "12.0", 44 | }; 45 | 46 | const VSWHERE_PATH = `${PROGRAM_FILES_X86}\\Microsoft Visual Studio\\Installer`; 47 | 48 | function vsversion_to_versionnumber(vsversion: string): string { 49 | if (Object.values(VsYearVersion).includes(vsversion)) { 50 | return vsversion; 51 | } else { 52 | if (vsversion in VsYearVersion) { 53 | return VsYearVersion[vsversion]; 54 | } 55 | } 56 | return vsversion; 57 | } 58 | 59 | function vsversion_to_year(vsversion: string): string { 60 | if (Object.keys(VsYearVersion).includes(vsversion)) { 61 | return vsversion; 62 | } else { 63 | for (const [year, ver] of Object.entries(VsYearVersion)) { 64 | if (ver === vsversion) { 65 | return year; 66 | } 67 | } 68 | } 69 | return vsversion; 70 | } 71 | 72 | function findWithVswhere( 73 | pattern: string, 74 | version_pattern: string, 75 | ): string | null { 76 | try { 77 | const installationPath = child_process 78 | .execSync( 79 | `vswhere -products * ${version_pattern} -prerelease -property installationPath`, 80 | ) 81 | .toString() 82 | .trim(); 83 | return `${installationPath}\\\\${pattern}`; 84 | } catch (e) { 85 | core.warning(`vswhere failed: ${e}`); 86 | } 87 | return null; 88 | } 89 | 90 | function findVcvarsall(vsversion: string): string { 91 | const vsversion_number = vsversion_to_versionnumber(vsversion); 92 | let version_pattern; 93 | if (vsversion_number) { 94 | const upper_bound = vsversion_number.split(".")[0] + ".9"; 95 | version_pattern = `-version "${vsversion_number},${upper_bound}"`; 96 | } else { 97 | version_pattern = "-latest"; 98 | } 99 | 100 | // If vswhere is available, ask it about the location of the latest Visual Studio. 101 | let path = findWithVswhere( 102 | "VC\\Auxiliary\\Build\\vcvarsall.bat", 103 | version_pattern, 104 | ); 105 | if (path && fs.existsSync(path)) { 106 | core.info(`Found with vswhere: ${path}`); 107 | return path; 108 | } 109 | core.info("Not found with vswhere"); 110 | 111 | // If that does not work, try the standard installation locations, 112 | // starting with the latest and moving to the oldest. 113 | const years = vsversion ? [vsversion_to_year(vsversion)] : YEARS; 114 | for (const prog_files of PROGRAM_FILES) { 115 | for (const ver of years) { 116 | for (const ed of EDITIONS) { 117 | path = `${prog_files}\\Microsoft Visual Studio\\${ver}\\${ed}\\VC\\Auxiliary\\Build\\vcvarsall.bat`; 118 | core.info(`Trying standard location: ${path}`); 119 | if (fs.existsSync(path)) { 120 | core.info(`Found standard location: ${path}`); 121 | return path; 122 | } 123 | } 124 | } 125 | } 126 | core.info("Not found in standard locations"); 127 | 128 | // Special case for Visual Studio 2015 (and maybe earlier), try it out too. 129 | path = `${PROGRAM_FILES_X86}\\Microsoft Visual C++ Build Tools\\vcbuildtools.bat`; 130 | if (fs.existsSync(path)) { 131 | core.info(`Found VS 2015: ${path}`); 132 | return path; 133 | } 134 | core.info(`Not found in VS 2015 location: ${path}`); 135 | 136 | throw new SetupSdlError("Microsoft Visual Studio not found"); 137 | } 138 | 139 | function isPathVariable(name: string): boolean { 140 | const pathLikeVariables = ["PATH", "INCLUDE", "LIB", "LIBPATH"]; 141 | return pathLikeVariables.indexOf(name.toUpperCase()) != -1; 142 | } 143 | 144 | function filterPathValue(path: string): string { 145 | const paths = path.split(";"); 146 | // Remove duplicates by keeping the first occurrence and preserving order. 147 | // This keeps path shadowing working as intended. 148 | function unique(value: string, index: number, self: string[]): boolean { 149 | return self.indexOf(value) === index; 150 | } 151 | return paths.filter(unique).join(";"); 152 | } 153 | 154 | /** See https://github.com/ilammy/msvc-dev-cmd#inputs */ 155 | function setupMSVCDevCmd( 156 | arch: string, 157 | sdk: string, 158 | toolset: boolean, 159 | uwp: boolean, 160 | spectre: boolean, 161 | vsversion: string, 162 | ) { 163 | if (process.platform != "win32") { 164 | core.info("This is not a Windows virtual environment, bye!"); 165 | return; 166 | } 167 | 168 | // Add standard location of "vswhere" to PATH, in case it"s not there. 169 | process.env.PATH += path.delimiter + VSWHERE_PATH; 170 | 171 | // There are all sorts of way the architectures are called. In addition to 172 | // values supported by Microsoft Visual C++, recognize some common aliases. 173 | const arch_aliases: { [key: string]: string } = { 174 | win32: "x86", 175 | win64: "x64", 176 | x86_64: "x64", 177 | "x86-64": "x64", 178 | }; 179 | // Ignore case when matching as that"s what humans expect. 180 | if (arch.toLowerCase() in arch_aliases) { 181 | arch = arch_aliases[arch.toLowerCase()]; 182 | } 183 | 184 | // Due to the way Microsoft Visual C++ is configured, we have to resort to the following hack: 185 | // Call the configuration batch file and then output *all* the environment variables. 186 | 187 | const args = [arch]; 188 | if (uwp) { 189 | args.push("uwp"); 190 | } 191 | if (sdk) { 192 | args.push(sdk); 193 | } 194 | if (toolset) { 195 | args.push(`-vcvars_ver=${toolset}`); 196 | } 197 | if (spectre) { 198 | args.push("-vcvars_spectre_libs=spectre"); 199 | } 200 | 201 | const vcvars = `"${findVcvarsall(vsversion)}" ${args.join(" ")}`; 202 | core.debug(`vcvars command-line: ${vcvars}`); 203 | 204 | const cmd_output_string = child_process 205 | .execSync(`set && cls && ${vcvars} && cls && set`, { shell: "cmd" }) 206 | .toString(); 207 | const cmd_output_parts = cmd_output_string.split("\f"); 208 | 209 | const old_environment = cmd_output_parts[0].split("\r\n"); 210 | const vcvars_output = cmd_output_parts[1].split("\r\n"); 211 | const new_environment = cmd_output_parts[2].split("\r\n"); 212 | 213 | // If vsvars.bat is given an incorrect command line, it will print out 214 | // an error and *still* exit successfully. Parse out errors from output 215 | // which don"t look like environment variables, and fail if appropriate. 216 | const error_messages = vcvars_output.filter((line) => { 217 | if (line.match(/^\[ERROR.*\]/)) { 218 | // Don"t print this particular line which will be confusing in output. 219 | if (!line.match(/Error in script usage. The correct usage is:$/)) { 220 | return true; 221 | } 222 | } 223 | return false; 224 | }); 225 | if (error_messages.length > 0) { 226 | throw new Error( 227 | "invalid parameters" + "\r\n" + error_messages.join("\r\n"), 228 | ); 229 | } 230 | 231 | const result_vcvars: { [key: string]: string } = {}; 232 | 233 | // Convert old environment lines into a dictionary for easier lookup. 234 | const old_env_vars: { [key: string]: string } = {}; 235 | for (const string of old_environment) { 236 | const [name, value] = string.split("="); 237 | old_env_vars[name] = value; 238 | } 239 | 240 | // Now look at the new environment and export everything that changed. 241 | // These are the variables set by vsvars.bat. Also export everything 242 | // that was not there during the first sweep: those are new variables. 243 | core.startGroup("Environment variables"); 244 | for (const string of new_environment) { 245 | const [key, vcvars_value] = string.split("="); 246 | // vsvars.bat likes to print some fluff at the beginning. 247 | // Skip lines that don"t look like environment variables. 248 | if (!vcvars_value) { 249 | continue; 250 | } 251 | const old_value = old_env_vars[key]; 252 | // For new variables "old_value === undefined". 253 | if (vcvars_value !== old_value) { 254 | let filtered_value = vcvars_value; 255 | core.info(`Setting ${key}`); 256 | // Special case for a bunch of PATH-like variables: vcvarsall.bat 257 | // just prepends its stuff without checking if its already there. 258 | // This makes repeated invocations of this action fail after some 259 | // point, when the environment variable overflows. Avoid that. 260 | if (isPathVariable(key)) { 261 | filtered_value = filterPathValue(vcvars_value); 262 | } 263 | 264 | result_vcvars[key] = filtered_value; 265 | } 266 | } 267 | core.endGroup(); 268 | 269 | core.info("Configured Developer Command Prompt"); 270 | 271 | return result_vcvars; 272 | } 273 | 274 | export function setup_vc_environment() { 275 | const arch = core.getInput("msvc-arch"); 276 | const sdk = core.getInput("vc_sdk"); 277 | const toolset = core.getBooleanInput("vc_toolset"); 278 | const uwp = core.getBooleanInput("vc_uwp"); 279 | const spectre = core.getBooleanInput("vc_spectre"); 280 | const vsversion = core.getInput("vc_vsversion"); 281 | 282 | const msvc_env_vars = setupMSVCDevCmd( 283 | arch, 284 | sdk, 285 | toolset, 286 | uwp, 287 | spectre, 288 | vsversion, 289 | ); 290 | 291 | for (const key in msvc_env_vars) { 292 | process.env[key] = msvc_env_vars[key]; 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/platform.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os"; 2 | import * as core from "@actions/core"; 3 | import * as path from "path"; 4 | 5 | import { SetupSdlError } from "./util"; 6 | 7 | export enum SdlBuildPlatform { 8 | Windows = "Windows", 9 | Linux = "Linux", 10 | Macos = "MacOS", 11 | } 12 | 13 | export function get_sdl_build_platform(): SdlBuildPlatform { 14 | switch (os.platform()) { 15 | case "linux": 16 | return SdlBuildPlatform.Linux; 17 | case "darwin": 18 | return SdlBuildPlatform.Macos; 19 | case "win32": 20 | return SdlBuildPlatform.Windows; 21 | } 22 | throw new SetupSdlError("Unsupported build platform"); 23 | } 24 | 25 | export function get_platform_root_directory( 26 | platform: SdlBuildPlatform, 27 | ): string { 28 | const root: null | string = core.getInput("root"); 29 | if (root) { 30 | return root; 31 | } 32 | switch (platform) { 33 | case SdlBuildPlatform.Windows: 34 | return "C:/setupsdl"; 35 | case SdlBuildPlatform.Macos: 36 | case SdlBuildPlatform.Linux: 37 | return `${os.tmpdir()}/setupsdl`; 38 | } 39 | } 40 | 41 | export function export_environment_variables( 42 | platform: SdlBuildPlatform, 43 | prefixes: string[], 44 | ) { 45 | switch (platform) { 46 | case SdlBuildPlatform.Windows: { 47 | const bin_paths = prefixes.map((prefix) => { 48 | return `${prefix}/bin`; 49 | }); 50 | const extra_path = bin_paths.join(path.delimiter); 51 | core.addPath(extra_path); 52 | break; 53 | } 54 | case SdlBuildPlatform.Macos: { 55 | const lib_paths = prefixes.map((prefix) => { 56 | return `${prefix}/lib`; 57 | }); 58 | let dyld_path = lib_paths.join(path.delimiter); 59 | if (process.env.DYLD_LIBRARY_PATH) { 60 | dyld_path += `:${process.env.DYLD_LIBRARY_PATH}`; 61 | } 62 | core.exportVariable("DYLD_LIBRARY_PATH", dyld_path); 63 | break; 64 | } 65 | case SdlBuildPlatform.Linux: { 66 | const lib_paths = prefixes.map((prefix) => { 67 | return `${prefix}/lib`; 68 | }); 69 | let ld_path = lib_paths.join(path.delimiter); 70 | if (process.env.LD_LIBRARY_PATH) { 71 | ld_path += `:${process.env.LD_LIBRARY_PATH}`; 72 | } 73 | core.exportVariable("LD_LIBRARY_PATH", ld_path); 74 | break; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/pm.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process"; 2 | 3 | import * as core from "@actions/core"; 4 | import { SdlBuildPlatform } from "./platform"; 5 | import { SetupSdlError } from "./util"; 6 | import { Executor } from "./main"; 7 | 8 | export enum PackageManagerType { 9 | Apk = "apk", 10 | AptGet = "apt-get", 11 | Brew = "brew", 12 | Dnf = "dnf", 13 | Pacman = "pacman", 14 | Msys2Pacman = "msys2-pacman", 15 | } 16 | 17 | export type Packages = { 18 | [key in PackageManagerType]?: { required: string[]; optional: string[] }; 19 | }; 20 | 21 | export function package_manager_type_from_string( 22 | text: string, 23 | ): PackageManagerType | undefined { 24 | switch (text.trim().toLowerCase()) { 25 | case "apk": 26 | case "alpine": 27 | return PackageManagerType.Apk; 28 | case "aptget": 29 | case "apt-get": 30 | case "ubuntu": 31 | case "debian": 32 | return PackageManagerType.AptGet; 33 | case "dnf": 34 | case "fedora": 35 | case "rhel": 36 | return PackageManagerType.Dnf; 37 | case "pacman": 38 | case "arch": 39 | return PackageManagerType.Pacman; 40 | } 41 | throw new SetupSdlError(`Unknown package manager "${text}"`); 42 | } 43 | 44 | abstract class PackageManager { 45 | type: PackageManagerType; 46 | sudo: boolean; 47 | executor: Executor; 48 | 49 | protected constructor(args: { 50 | type: PackageManagerType; 51 | executor: Executor; 52 | sudo?: boolean; 53 | }) { 54 | this.type = args.type; 55 | this.executor = args.executor; 56 | this.sudo = args.sudo == undefined ? command_exists("sudo") : args.sudo; 57 | } 58 | abstract update(): void; 59 | abstract install(packages: string[]): void; 60 | 61 | maybe_sudo_execute(command: string) { 62 | command = (this.sudo ? " sudo " : "") + command; 63 | core.info(`Executing "${command}"`); 64 | this.executor.run(command, true); 65 | } 66 | } 67 | 68 | class AptGetPackageManager extends PackageManager { 69 | constructor(executor: Executor) { 70 | super({ executor: executor, type: PackageManagerType.AptGet }); 71 | } 72 | 73 | update() { 74 | this.maybe_sudo_execute("apt-get update -y"); 75 | } 76 | 77 | install(packages: string[]) { 78 | this.maybe_sudo_execute(`apt-get install -y ${packages.join(" ")}`); 79 | } 80 | } 81 | 82 | class DnfPackageManager extends PackageManager { 83 | constructor(executor: Executor) { 84 | super({ executor: executor, type: PackageManagerType.Dnf }); 85 | } 86 | 87 | update() { 88 | // Not needed 89 | } 90 | 91 | install(packages: string[]) { 92 | this.maybe_sudo_execute(`dnf install -y ${packages.join(" ")}`); 93 | } 94 | } 95 | 96 | class ApkPackageManager extends PackageManager { 97 | constructor(executor: Executor) { 98 | super({ executor: executor, type: PackageManagerType.Apk }); 99 | } 100 | 101 | update() { 102 | // Not needed 103 | } 104 | 105 | install(packages: string[]) { 106 | this.maybe_sudo_execute(`apk add ${packages.join(" ")}`); 107 | } 108 | } 109 | 110 | class BrewPackageManager extends PackageManager { 111 | constructor(executor: Executor) { 112 | super({ executor: executor, type: PackageManagerType.Apk }); 113 | } 114 | 115 | update() { 116 | this.maybe_sudo_execute(`brew update`); 117 | } 118 | 119 | install(packages: string[]) { 120 | this.maybe_sudo_execute(`brew install -y ${packages.join(" ")}`); 121 | } 122 | } 123 | 124 | class PacmanPackageManager extends PackageManager { 125 | constructor(executor: Executor) { 126 | super({ executor: executor, type: PackageManagerType.Pacman }); 127 | } 128 | 129 | update() { 130 | this.maybe_sudo_execute(`pacman -Sy`); 131 | } 132 | 133 | install(packages: string[]) { 134 | this.maybe_sudo_execute(`pacman --noconfirm -S ${packages.join(" ")}`); 135 | } 136 | } 137 | 138 | class Msys2PacmanPackageManager extends PackageManager { 139 | prefix: string; 140 | constructor(executor: Executor) { 141 | super({ executor: executor, type: PackageManagerType.Pacman, sudo: false }); 142 | const msystem = process.env.MSYSTEM; 143 | if (!msystem) { 144 | throw new SetupSdlError( 145 | "msys2-pacman requires MSYSTEM environment variable", 146 | ); 147 | } 148 | const msystem_lower = msystem.toLowerCase(); 149 | if (msystem_lower === "mingw32") { 150 | this.prefix = "mingw-w64-i686-"; 151 | } else if (msystem_lower === "mingw64") { 152 | this.prefix = "mingw-w64-x86_64-"; 153 | } else if (msystem_lower === "clang32") { 154 | this.prefix = "mingw-w64-clang-i686-"; 155 | } else if (msystem_lower === "clang64") { 156 | this.prefix = "mingw-w64-clang-x86_64-"; 157 | } else if (msystem_lower === "ucrt64") { 158 | this.prefix = "mingw-w64-ucrt-x86_64-"; 159 | } else { 160 | throw new SetupSdlError(`Invalid MSYSTEM=${msystem}`); 161 | } 162 | } 163 | 164 | update() { 165 | this.maybe_sudo_execute(`pacman -Sy`); 166 | } 167 | 168 | install(packages: string[]) { 169 | const prepended_packages = packages.map((p) => `${this.prefix}${p}`); 170 | this.maybe_sudo_execute( 171 | `pacman --noconfirm -S ${prepended_packages.join(" ")}`, 172 | ); 173 | } 174 | } 175 | 176 | export function create_package_manager(args: { 177 | type: PackageManagerType; 178 | executor: Executor; 179 | }): PackageManager { 180 | switch (args.type) { 181 | case PackageManagerType.AptGet: 182 | return new AptGetPackageManager(args.executor); 183 | case PackageManagerType.Apk: 184 | return new ApkPackageManager(args.executor); 185 | case PackageManagerType.Brew: 186 | return new BrewPackageManager(args.executor); 187 | case PackageManagerType.Pacman: 188 | return new PacmanPackageManager(args.executor); 189 | case PackageManagerType.Msys2Pacman: 190 | return new Msys2PacmanPackageManager(args.executor); 191 | case PackageManagerType.Dnf: 192 | return new DnfPackageManager(args.executor); 193 | } 194 | } 195 | 196 | function command_exists(name: string): boolean { 197 | try { 198 | child_process.execSync(`command -v ${name}`); 199 | return true; 200 | } catch { 201 | return false; 202 | } 203 | } 204 | 205 | export function detect_package_manager(args: { 206 | build_platform: SdlBuildPlatform; 207 | }): PackageManagerType | undefined { 208 | if (args.build_platform == SdlBuildPlatform.Windows) { 209 | if (process.env.MSYSTEM) { 210 | return PackageManagerType.Msys2Pacman; 211 | } 212 | return undefined; 213 | } else if (args.build_platform == SdlBuildPlatform.Macos) { 214 | return PackageManagerType.Brew; 215 | } 216 | if (command_exists("apt-get")) { 217 | return PackageManagerType.AptGet; 218 | } else if (command_exists("apk")) { 219 | return PackageManagerType.Apk; 220 | } else if (command_exists("pacman")) { 221 | return PackageManagerType.Pacman; 222 | } else if (command_exists("dnf")) { 223 | return PackageManagerType.Dnf; 224 | } 225 | return undefined; 226 | } 227 | -------------------------------------------------------------------------------- /src/repo.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import { Octokit } from "@octokit/rest"; 3 | 4 | import { SetupSdlError } from "./util"; 5 | 6 | export async function convert_git_branch_tag_to_hash(args: { 7 | branch_or_hash: string; 8 | owner: string; 9 | repo: string; 10 | octokit: Octokit; 11 | }): Promise { 12 | return await core.group( 13 | `Calculating git hash of ${args.owner}/${args.repo}:${args.branch_or_hash}`, 14 | async () => { 15 | try { 16 | core.debug(`Look for a branch named "${args.branch_or_hash}"...`); 17 | const response = await args.octokit.rest.repos.getBranch({ 18 | owner: args.owner, 19 | repo: args.repo, 20 | branch: args.branch_or_hash, 21 | }); 22 | core.debug("It was a branch."); 23 | const sha = response.data.commit.sha; 24 | core.info(`git hash = ${sha}`); 25 | return sha; 26 | } catch { 27 | core.debug("It was not a branch."); 28 | } 29 | try { 30 | core.debug(`Look for a commit named "${args.branch_or_hash}"...`); 31 | const response = await args.octokit.rest.repos.getCommit({ 32 | owner: args.owner, 33 | repo: args.repo, 34 | ref: args.branch_or_hash, 35 | }); 36 | core.debug("It was a commit."); 37 | return response.data.sha; 38 | } catch { 39 | core.debug("It was not a commit."); 40 | } 41 | throw new SetupSdlError( 42 | `Unable to convert ${args.branch_or_hash} into a git hash.`, 43 | ); 44 | }, 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/util.test.ts: -------------------------------------------------------------------------------- 1 | import { shlex_split } from "./util"; 2 | 3 | import { describe, expect, test } from "@jest/globals"; 4 | 5 | describe("testing shlex.parse", () => { 6 | test("test undefined", () => { 7 | expect(shlex_split(undefined)).toStrictEqual([]); 8 | }); 9 | test("test empty string", () => { 10 | expect(shlex_split("")).toStrictEqual([]); 11 | }); 12 | test("test string with whitespace(s)", () => { 13 | expect(shlex_split(" ")).toStrictEqual([]); 14 | expect(shlex_split(" ")).toStrictEqual([]); 15 | expect(shlex_split("\t")).toStrictEqual([]); 16 | expect(shlex_split(" \t")).toStrictEqual([]); 17 | expect(shlex_split("\t\t \t")).toStrictEqual([]); 18 | }); 19 | test("test simple string with text", () => { 20 | expect(shlex_split("a")).toStrictEqual(["a"]); 21 | expect(shlex_split("a b")).toStrictEqual(["a", "b"]); 22 | expect(shlex_split(" a \t \t b ")).toStrictEqual(["a", "b"]); 23 | }); 24 | test("test string with escape characters", () => { 25 | expect(shlex_split('"a"')).toStrictEqual(["a"]); 26 | expect(shlex_split('"a" "b"')).toStrictEqual(["a", "b"]); 27 | expect(shlex_split('"a b" ')).toStrictEqual(["a b"]); 28 | }); 29 | test("test example extra cmake arguments", () => { 30 | expect(shlex_split("-A win32")).toStrictEqual(["-A", "win32"]); 31 | expect(shlex_split("-DSDL_STATIC=ON -DSDL_X11=OFF")).toStrictEqual([ 32 | "-DSDL_STATIC=ON", 33 | "-DSDL_X11=OFF", 34 | ]); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import * as shlex from "shlex"; 2 | 3 | export class SetupSdlError extends Error { 4 | constructor(message: string) { 5 | super(message); 6 | } 7 | } 8 | 9 | export function shlex_split(text: undefined | string): string[] { 10 | if (!text) { 11 | return []; 12 | } else { 13 | text = text.trim(); 14 | if (text == "") { 15 | return []; 16 | } 17 | return shlex.split(text); 18 | } 19 | } 20 | 21 | export function command_arglist_to_string(args: string[]): string { 22 | return args.map((s) => `"${s}"`).join(" "); 23 | } 24 | -------------------------------------------------------------------------------- /src/version.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GitHubRelease, 3 | parse_version_string, 4 | Version, 5 | ReleaseDb, 6 | ReleaseType, 7 | } from "./version"; 8 | 9 | import { describe, expect, test } from "@jest/globals"; 10 | 11 | const GH_RELEASE_OUTPUT = 12 | "" + 13 | "3.1.1\tLatest\tprerelease-3.1.1\t2023-12-25T18:45:17Z\n" + 14 | "2.28.0\tLatest\trelease-2.28.0\t2023-06-20T18:45:17Z\n" + 15 | "2.28.0 RC1\tPre-release\tprerelease-2.27.1\t2023-06-14T03:59:14Z\n" + 16 | "2.26.5\t\trelease-2.26.5\t2023-04-05T19:35:40Z\n" + 17 | "2.26.4\t\trelease-2.26.4\t2023-03-07T00:17:02Z\n" + 18 | "2.26.3\t\trelease-2.26.3\t2023-02-06T23:31:56Z\n" + 19 | "2.26.2\t\trelease-2.26.2\t2023-01-03T15:08:11Z\n" + 20 | "2.26.1\t\trelease-2.26.1\t2022-12-01T20:33:11Z\n" + 21 | "2.26.0\t\trelease-2.26.0\t2022-11-22T00:28:26Z\n" + 22 | "2.26.0 RC1\tPre-release\tprerelease-2.25.1\t2022-11-17T17:49:02Z\n" + 23 | "2.24.2\t\trelease-2.24.2\t2022-11-01T13:39:15Z\n" + 24 | "2.24.1\t\trelease-2.24.1\t2022-10-05T00:16:33Z\n" + 25 | "2.24.0\t\trelease-2.24.0\t2022-08-19T16:04:03Z\n" + 26 | "2.0.22\t\trelease-2.0.22\t2022-04-25T19:20:25Z\n" + 27 | "2.0.20\t\trelease-2.0.20\t2022-01-11T01:03:58Z\n" + 28 | "2.0.18\t\trelease-2.0.18\t2021-11-30T17:15:42Z\n" + 29 | "2.0.16\t\trelease-2.0.16\t2021-08-10T16:03:15Z\n" + 30 | "2.0.14\t\trelease-2.0.14\t2021-07-08T17:14:16Z\n" + 31 | "2.0.12\t\trelease-2.0.12\t2022-05-24T22:37:24Z\n" + 32 | "2.0.10\t\trelease-2.0.10\t2022-05-24T22:35:08Z\n" + 33 | "2.0.9\t\trelease-2.0.9\t2022-05-24T22:33:03Z\n" + 34 | "2.0.8\t\trelease-2.0.8\t2022-05-23T22:20:21Z\n"; 35 | describe("testing parsing of version string", () => { 36 | function add_parse_to_version_test( 37 | input: string, 38 | major: number, 39 | minor: number, 40 | patch: number, 41 | type: ReleaseType, 42 | ) { 43 | test(`test ${input}`, () => { 44 | const result = parse_version_string(input, "sdl"); 45 | expect(result).toBeTruthy(); 46 | if (result) { 47 | expect(result.type).toStrictEqual(type); 48 | expect(result.version).toStrictEqual( 49 | new Version({ major: major, minor: minor, patch: patch }), 50 | ); 51 | } 52 | }); 53 | } 54 | function add_parse_to_commit_test(input: string) { 55 | test(`test ${input}`, () => { 56 | const result = parse_version_string(input, "sdl"); 57 | expect(result).toBeTruthy(); 58 | if (result) { 59 | expect(result.type).toStrictEqual(ReleaseType.Commit); 60 | expect(result.version).toStrictEqual(input); 61 | } 62 | }); 63 | } 64 | 65 | add_parse_to_version_test("2-any", 2, 0, 0, ReleaseType.Any); 66 | add_parse_to_version_test("sdl2-any", 2, 0, 0, ReleaseType.Any); 67 | add_parse_to_version_test("SDL2-any", 2, 0, 0, ReleaseType.Any); 68 | add_parse_to_version_test("3-any", 3, 0, 0, ReleaseType.Any); 69 | add_parse_to_version_test("SDL3-any", 3, 0, 0, ReleaseType.Any); 70 | add_parse_to_version_test("sdl3-any", 3, 0, 0, ReleaseType.Any); 71 | 72 | add_parse_to_version_test("2-head", 2, 0, 0, ReleaseType.Head); 73 | add_parse_to_version_test("sdl2-head", 2, 0, 0, ReleaseType.Head); 74 | add_parse_to_version_test("SDL2-head", 2, 0, 0, ReleaseType.Head); 75 | add_parse_to_version_test("3-head", 3, 0, 0, ReleaseType.Head); 76 | add_parse_to_version_test("SDL3-head", 3, 0, 0, ReleaseType.Head); 77 | add_parse_to_version_test("sdl3-head", 3, 0, 0, ReleaseType.Head); 78 | 79 | add_parse_to_version_test("2-latest", 2, 0, 0, ReleaseType.Latest); 80 | add_parse_to_version_test("sdl2-latest", 2, 0, 0, ReleaseType.Latest); 81 | add_parse_to_version_test("SDL2-latest", 2, 0, 0, ReleaseType.Latest); 82 | add_parse_to_version_test("3-latest", 3, 0, 0, ReleaseType.Latest); 83 | add_parse_to_version_test("SDL3-latest", 3, 0, 0, ReleaseType.Latest); 84 | add_parse_to_version_test("sdl3-latest", 3, 0, 0, ReleaseType.Latest); 85 | 86 | add_parse_to_version_test("2.22.1", 2, 22, 1, ReleaseType.Exact); 87 | add_parse_to_version_test("2.0.18", 2, 0, 18, ReleaseType.Exact); 88 | add_parse_to_version_test("3.2.0", 3, 2, 0, ReleaseType.Exact); 89 | add_parse_to_version_test("3.2.2", 3, 2, 2, ReleaseType.Exact); 90 | add_parse_to_version_test("SDL2.22.1", 2, 22, 1, ReleaseType.Exact); 91 | add_parse_to_version_test("SDL2.0.18", 2, 0, 18, ReleaseType.Exact); 92 | add_parse_to_version_test("SDL3.2.0", 3, 2, 0, ReleaseType.Exact); 93 | add_parse_to_version_test("SDL3.2.2", 3, 2, 2, ReleaseType.Exact); 94 | 95 | add_parse_to_commit_test("f168f9c81326ad374aade49d1dc46f245b20d07a"); 96 | add_parse_to_commit_test("main"); 97 | add_parse_to_commit_test("SDL2"); 98 | }); 99 | 100 | describe("test finding a release", () => { 101 | const github_releases = GitHubRelease.from_gh_output(GH_RELEASE_OUTPUT); 102 | const sdl_release_db = ReleaseDb.create(github_releases); 103 | expect(sdl_release_db.releases).toBeTruthy(); 104 | 105 | test(`test finding exact 2.0.22 release`, () => { 106 | const v = new Version({ major: 2, minor: 0, patch: 22 }); 107 | const rel = sdl_release_db.find(v, true, ReleaseType.Exact); 108 | expect(rel).not.toBeNull(); 109 | if (rel) { 110 | expect(rel.version).toStrictEqual(v); 111 | expect(rel.prerelease).toBeFalsy(); 112 | } 113 | }); 114 | 115 | test(`test finding exact 2.26.1 release`, () => { 116 | const v = new Version({ major: 2, minor: 26, patch: 1 }); 117 | const rel = sdl_release_db.find(v, true, ReleaseType.Exact); 118 | expect(rel).not.toBeNull(); 119 | if (rel) { 120 | expect(rel.version).toStrictEqual(v); 121 | expect(rel.prerelease).toBeFalsy(); 122 | } 123 | }); 124 | 125 | test(`test finding latest 2 release`, () => { 126 | const v = new Version({ major: 2, minor: 0, patch: 0 }); 127 | const rel = sdl_release_db.find(v, true, ReleaseType.Latest); 128 | expect(rel).not.toBeNull(); 129 | if (rel) { 130 | // 2.26.5 exists, so the result must be > 2.26.4 131 | expect(rel.version.compare(new Version("2.26.4"))).toBeLessThan(0); 132 | expect(rel.version.major).toBe(2); 133 | } 134 | }); 135 | 136 | test(`test finding latest non-prerelease 2 release`, () => { 137 | const v = new Version({ major: 2, minor: 0, patch: 0 }); 138 | const rel = sdl_release_db.find(v, false, ReleaseType.Latest); 139 | expect(rel).not.toBeNull(); 140 | if (rel) { 141 | // 2.26.5 exists, so the result must be > 2.26.4 142 | expect(rel.version.compare(new Version("2.26.4"))).toBeLessThan(0); 143 | expect(rel.version.major).toBe(2); 144 | expect(rel.prerelease).toBeFalsy(); 145 | } 146 | }); 147 | 148 | test(`test finding any 2 release`, () => { 149 | const v = new Version({ major: 2, minor: 0, patch: 0 }); 150 | const rel = sdl_release_db.find(v, true, ReleaseType.Any); 151 | expect(rel).not.toBeNull(); 152 | if (rel) { 153 | // 2.26.5 exists, so the result must be > 2.26.4 154 | expect(rel.version.major).toBe(2); 155 | } 156 | }); 157 | 158 | test(`test finding any 3 release`, () => { 159 | const v = new Version({ major: 3, minor: 0, patch: 0 }); 160 | const rel = sdl_release_db.find(v, true, ReleaseType.Any); 161 | expect(rel).not.toBeNull(); 162 | if (rel) { 163 | // FIXME: Only 3.0.0-prerelease exists at the moment 164 | expect(rel.version.compare(new Version("3.0.0"))).toBeLessThanOrEqual(0); 165 | expect(rel.version.major).toBe(3); 166 | } 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process"; 2 | import * as fs from "fs"; 3 | 4 | import * as pm from "./pm"; 5 | import { SetupSdlError } from "./util"; 6 | 7 | export class GitHubRelease { 8 | name: string; 9 | prerelease: boolean; 10 | tag: string; 11 | time: number; 12 | constructor(name: string, prerelease: boolean, tag: string, time: number) { 13 | this.name = name; 14 | this.prerelease = prerelease; 15 | this.tag = tag; 16 | this.time = time; 17 | } 18 | 19 | static fetch_all(repo: string): GitHubRelease[] { 20 | const buffer = child_process.execSync(`gh release list -R ${repo} -L 1000`); 21 | return GitHubRelease.from_gh_output(buffer.toString()); 22 | } 23 | 24 | static from_gh_output(text: string): GitHubRelease[] { 25 | return text 26 | .trim() 27 | .split("\n") 28 | .map((line_str) => { 29 | const line_parts = line_str.split("\t"); 30 | return new GitHubRelease( 31 | line_parts[0], 32 | line_parts[1].toLowerCase() == "pre-release", 33 | line_parts[2], 34 | Date.parse(line_parts[3]), 35 | ); 36 | }); 37 | } 38 | } 39 | 40 | export class Version { 41 | major: number; 42 | minor: number; 43 | patch: number; 44 | constructor( 45 | version: string | { major: number; minor: number; patch: number }, 46 | ) { 47 | if (typeof version == "string") { 48 | const v_list = version.split("."); 49 | if (v_list.length == 0 || v_list.length > 3) { 50 | throw new SetupSdlError( 51 | `Cannot convert version (${version}) to MAJOR.MINOR.PATCH`, 52 | ); 53 | } 54 | this.major = Number(v_list[0]); 55 | if (v_list.length > 0) { 56 | this.minor = Number(v_list[1]); 57 | } else { 58 | this.minor = 0; 59 | } 60 | if (v_list.length > 1) { 61 | this.patch = Number(v_list[2]); 62 | } else { 63 | this.patch = 0; 64 | } 65 | } else { 66 | this.major = version.major; 67 | this.minor = version.minor; 68 | this.patch = version.patch; 69 | } 70 | if (isNaN(this.major) || isNaN(this.minor) || isNaN(this.patch)) { 71 | throw new SetupSdlError( 72 | `Cannot convert version (${version}) to MAJOR.MINOR.PATCH`, 73 | ); 74 | } 75 | } 76 | 77 | compare(other: Version): number { 78 | if (this.major > other.major) { 79 | return -1; 80 | } 81 | if (other.major > this.major) { 82 | return 1; 83 | } 84 | 85 | if (this.minor > other.minor) { 86 | return -1; 87 | } 88 | if (other.minor > this.minor) { 89 | return 1; 90 | } 91 | 92 | if (this.patch > other.patch) { 93 | return -1; 94 | } 95 | if (other.patch > this.patch) { 96 | return 1; 97 | } 98 | 99 | return 0; 100 | } 101 | 102 | equals(other: Version): boolean { 103 | return this.compare(other) == 0; 104 | } 105 | 106 | toString(): string { 107 | return `${this.major}.${this.minor}.${this.patch}`; 108 | } 109 | } 110 | 111 | export enum Project { 112 | SDL = "SDL", 113 | SDL2_compat = "SDL2_compat", 114 | SDL12_compat = "SDL12_compat", 115 | SDL_image = "SDL_image", 116 | SDL_mixer = "SDL_mixer", 117 | SDL_net = "SDL_net", 118 | SDL_rtf = "SDL_rtf", 119 | SDL_ttf = "SDL_ttf", 120 | } 121 | 122 | interface ProjectDescription { 123 | option_name: string; 124 | discarded_prefix?: string; 125 | cmake_var_out_prefix: string; 126 | cmake_var_out_suffix: string; 127 | deps: Project[]; 128 | major_define: string; 129 | minor_define: string; 130 | patch_define: string; 131 | header_paths: string[]; 132 | header_filenames: string[]; 133 | git_url: string; 134 | repo_owner: string; 135 | repo_name: string; 136 | version_branch_map: { [version: number]: string }; 137 | packages?: pm.Packages; 138 | } 139 | 140 | export class VersionExtractor { 141 | major_define: string; 142 | minor_define: string; 143 | patch_define: string; 144 | header_paths: string[]; 145 | header_filenames: string[]; 146 | 147 | constructor(desc: ProjectDescription) { 148 | this.major_define = desc.major_define; 149 | this.minor_define = desc.minor_define; 150 | this.patch_define = desc.patch_define; 151 | this.header_paths = desc.header_paths; 152 | this.header_filenames = desc.header_filenames; 153 | } 154 | 155 | extract_from_header_path(path: string): Version | null { 156 | if (!fs.existsSync(path)) { 157 | throw new SetupSdlError(`Cannot find ${path}`); 158 | } 159 | 160 | const contents = fs.readFileSync(path, "utf8"); 161 | 162 | const match_major = contents.match( 163 | new RegExp(`#define[ \\t]+${this.major_define}[ \\t]+([0-9]+)`), 164 | ); 165 | if (!match_major) { 166 | return null; 167 | } 168 | const major_version = Number(match_major[1]); 169 | 170 | const match_minor = contents.match( 171 | new RegExp(`#define[ \\t]+${this.minor_define}[ \\t]+([0-9]+)`), 172 | ); 173 | if (!match_minor) { 174 | return null; 175 | } 176 | const minor_version = Number(match_minor[1]); 177 | 178 | const match_patch = contents.match( 179 | new RegExp(`#define[ \\t]+${this.patch_define}[ \\t]+([0-9]+)`), 180 | ); 181 | if (!match_patch) { 182 | return null; 183 | } 184 | const patch_version = Number(match_patch[1]); 185 | 186 | return new Version({ 187 | major: major_version, 188 | minor: minor_version, 189 | patch: patch_version, 190 | }); 191 | } 192 | 193 | extract_from_install_prefix(path: string): Version { 194 | const version = (() => { 195 | for (const infix_path of this.header_paths) { 196 | for (const header_filename of this.header_filenames) { 197 | const hdr_path = `${path}/${infix_path}/${header_filename}`; 198 | if (!fs.existsSync(hdr_path)) { 199 | continue; 200 | } 201 | const version = this.extract_from_header_path(hdr_path); 202 | if (version == null) { 203 | continue; 204 | } 205 | return version; 206 | } 207 | } 208 | throw new SetupSdlError(`Could not extract version from ${path}.`); 209 | })(); 210 | return version; 211 | } 212 | } 213 | 214 | export const project_descriptions: { [key in Project]: ProjectDescription } = { 215 | [Project.SDL]: { 216 | option_name: "version", 217 | discarded_prefix: "sdl", 218 | cmake_var_out_prefix: "SDL", 219 | cmake_var_out_suffix: "_ROOT", 220 | deps: [], 221 | major_define: "SDL_MAJOR_VERSION", 222 | minor_define: "SDL_MINOR_VERSION", 223 | patch_define: "(?:SDL_PATCHLEVEL|SDL_MICRO_VERSION)", 224 | header_paths: ["include/SDL3", "include/SDL2"], 225 | header_filenames: ["SDL_version.h"], 226 | git_url: "https://github.com/libsdl-org/SDL.git", 227 | repo_owner: "libsdl-org", 228 | repo_name: "SDL", 229 | version_branch_map: { 2: "SDL2", 3: "main" }, 230 | packages: { 231 | [pm.PackageManagerType.AptGet]: { 232 | required: [ 233 | "cmake", 234 | "make", 235 | "ninja-build", 236 | "libasound2-dev", 237 | "libpulse-dev", 238 | "libaudio-dev", 239 | "libjack-dev", 240 | "libsndio-dev", 241 | "libusb-1.0-0-dev", 242 | "libx11-dev", 243 | "libxext-dev", 244 | "libxrandr-dev", 245 | "libxcursor-dev", 246 | "libxfixes-dev", 247 | "libxi-dev", 248 | "libxss-dev", 249 | "libwayland-dev", 250 | "libxkbcommon-dev", 251 | "libdrm-dev", 252 | "libgbm-dev", 253 | "libgl1-mesa-dev", 254 | "libgles2-mesa-dev", 255 | "libegl1-mesa-dev", 256 | "libdbus-1-dev", 257 | "libibus-1.0-dev", 258 | "libudev-dev", 259 | "fcitx-libs-dev", 260 | ], 261 | optional: [ 262 | "libpipewire-0.3-dev" /* Ubuntu 22.04 */, 263 | "libdecor-0-dev" /* Ubuntu 22.04 */, 264 | ], 265 | }, 266 | [pm.PackageManagerType.Dnf]: { 267 | required: [ 268 | "cmake", 269 | "make", 270 | "ninja-build", 271 | "alsa-lib-devel", 272 | "dbus-devel", 273 | "ibus-devel", 274 | "libusb1-devel", 275 | "libX11-devel", 276 | "libXau-devel", 277 | "libXScrnSaver-devel", 278 | "libXcursor-devel", 279 | "libXext-devel", 280 | "libXfixes-devel", 281 | "libXi-devel", 282 | "libXrandr-devel", 283 | "libxkbcommon-devel", 284 | "libdecor-devel", 285 | "libglvnd-devel", 286 | "pipewire-devel", 287 | "pipewire-jack-audio-connection-kit-devel", 288 | "pulseaudio-libs-devel", 289 | "wayland-devel", 290 | ], 291 | optional: [], 292 | }, 293 | }, 294 | }, 295 | [Project.SDL_image]: { 296 | option_name: "version-sdl-image", 297 | cmake_var_out_prefix: "SDL", 298 | cmake_var_out_suffix: "_image_ROOT", 299 | deps: [Project.SDL], 300 | major_define: "SDL_IMAGE_MAJOR_VERSION", 301 | minor_define: "SDL_IMAGE_MINOR_VERSION", 302 | patch_define: "(?:SDL_IMAGE_MICRO_VERSION|SDL_IMAGE_PATCHLEVEL)", 303 | header_paths: ["include/SDL3_image", "include/SDL2"], 304 | header_filenames: ["SDL_image.h"], 305 | git_url: "https://github.com/libsdl-org/SDL_image.git", 306 | repo_owner: "libsdl-org", 307 | repo_name: "SDL_image", 308 | version_branch_map: { 2: "SDL2", 3: "main" }, 309 | }, 310 | [Project.SDL_mixer]: { 311 | option_name: "version-sdl-mixer", 312 | cmake_var_out_prefix: "SDL", 313 | cmake_var_out_suffix: "_mixer_ROOT", 314 | deps: [Project.SDL], 315 | major_define: "SDL_MIXER_MAJOR_VERSION", 316 | minor_define: "SDL_MIXER_MINOR_VERSION", 317 | patch_define: "(?:SDL_MIXER_MICRO_VERSION|SDL_MIXER_PATCHLEVEL)", 318 | header_paths: ["include/SDL3_mixer", "include/SDL2"], 319 | header_filenames: ["SDL_mixer.h"], 320 | git_url: "https://github.com/libsdl-org/SDL_mixer.git", 321 | repo_owner: "libsdl-org", 322 | repo_name: "SDL_mixer", 323 | version_branch_map: { 2: "SDL2", 3: "main" }, 324 | }, 325 | [Project.SDL_net]: { 326 | option_name: "version-sdl-net", 327 | cmake_var_out_prefix: "SDL", 328 | cmake_var_out_suffix: "_net_ROOT", 329 | deps: [Project.SDL], 330 | major_define: "SDL_NET_MAJOR_VERSION", 331 | minor_define: "SDL_NET_MINOR_VERSION", 332 | patch_define: "(?:SDL_NET_MICRO_VERSION|SDL_NET_PATCHLEVEL)", 333 | header_paths: ["include/SDL3_net", "include/SDL2", "include"], 334 | header_filenames: ["SDL_net.h"], 335 | git_url: "https://github.com/libsdl-org/SDL_net.git", 336 | repo_owner: "libsdl-org", 337 | repo_name: "SDL_net", 338 | version_branch_map: { 2: "SDL2", 3: "main" }, 339 | }, 340 | [Project.SDL_rtf]: { 341 | option_name: "version-sdl-rtf", 342 | cmake_var_out_prefix: "SDL", 343 | cmake_var_out_suffix: "_rtf_ROOT", 344 | deps: [Project.SDL, Project.SDL_ttf], 345 | major_define: "SDL_RTF_MAJOR_VERSION", 346 | minor_define: "SDL_RTF_MINOR_VERSION", 347 | patch_define: "(?:SDL_RTF_MICRO_VERSION|SDL_RTF_PATCHLEVEL)", 348 | header_paths: ["include/SDL3_rtf", "include/SDL2", "include"], 349 | header_filenames: ["SDL_rtf.h"], 350 | git_url: "https://github.com/libsdl-org/SDL_rtf.git", 351 | repo_owner: "libsdl-org", 352 | repo_name: "SDL_rtf", 353 | version_branch_map: { 2: "SDL2", 3: "main" }, 354 | }, 355 | [Project.SDL_ttf]: { 356 | option_name: "version-sdl-ttf", 357 | cmake_var_out_prefix: "SDL", 358 | cmake_var_out_suffix: "_ttf_ROOT", 359 | deps: [Project.SDL], 360 | major_define: "SDL_TTF_MAJOR_VERSION", 361 | minor_define: "SDL_TTF_MINOR_VERSION", 362 | patch_define: "(?:SDL_TTF_MICRO_VERSION|SDL_TTF_PATCHLEVEL)", 363 | header_paths: ["include/SDL3_ttf", "include/SDL2"], 364 | header_filenames: ["SDL_ttf.h"], 365 | git_url: "https://github.com/libsdl-org/SDL_ttf.git", 366 | repo_owner: "libsdl-org", 367 | repo_name: "SDL_ttf", 368 | version_branch_map: { 2: "SDL2", 3: "main" }, 369 | packages: { 370 | [pm.PackageManagerType.AptGet]: { 371 | required: ["libfreetype-dev", "libharfbuzz-dev"], 372 | optional: [], 373 | }, 374 | [pm.PackageManagerType.Dnf]: { 375 | required: ["freetype-devel", "harfbuzz-devel"], 376 | optional: [], 377 | }, 378 | [pm.PackageManagerType.Msys2Pacman]: { 379 | required: ["freetype", "harfbuzz"], 380 | optional: [], 381 | }, 382 | }, 383 | }, 384 | [Project.SDL2_compat]: { 385 | option_name: "version-sdl2-compat", 386 | cmake_var_out_prefix: "SDL", 387 | cmake_var_out_suffix: "_ROOT", 388 | deps: [Project.SDL], 389 | major_define: "SDL_MAJOR_VERSION", 390 | minor_define: "SDL_MINOR_VERSION", 391 | patch_define: "SDL_PATCHLEVEL", 392 | header_paths: ["include/SDL2"], 393 | header_filenames: ["SDL_version.h"], 394 | git_url: "https://github.com/libsdl-org/sdl2-compat.git", 395 | repo_owner: "libsdl-org", 396 | repo_name: "sdl2-compat", 397 | version_branch_map: { 2: "main" }, 398 | }, 399 | [Project.SDL12_compat]: { 400 | option_name: "version-sdl12-compat", 401 | cmake_var_out_prefix: "SDL", 402 | cmake_var_out_suffix: "_ROOT", 403 | deps: [Project.SDL2_compat], 404 | major_define: "SDL_MAJOR_VERSION", 405 | minor_define: "SDL_MINOR_VERSION", 406 | patch_define: "SDL_PATCHLEVEL", 407 | header_paths: ["include/SDL"], 408 | header_filenames: ["SDL_version.h"], 409 | git_url: "https://github.com/libsdl-org/sdl12-compat.git", 410 | repo_owner: "libsdl-org", 411 | repo_name: "sdl12-compat", 412 | version_branch_map: { 1: "main" }, 413 | }, 414 | }; 415 | 416 | export enum ReleaseType { 417 | Any = "Any", 418 | Head = "Head", 419 | Latest = "Latest", 420 | Exact = "Exact", 421 | Commit = "Commit", 422 | } 423 | 424 | // FIXME: rename to ReleaseDb + rename SdlRelease to Release 425 | export class ReleaseDb { 426 | releases: SdlRelease[]; 427 | 428 | constructor(releases: SdlRelease[]) { 429 | this.releases = releases; 430 | } 431 | 432 | find( 433 | version: Version, 434 | prerelease: boolean, 435 | type: ReleaseType, 436 | ): SdlRelease | null { 437 | for (const release of this.releases) { 438 | // Skip if a pre-release has not been requested 439 | if (release.prerelease != null && !prerelease) { 440 | continue; 441 | } 442 | if (type == ReleaseType.Exact) { 443 | if (release.version.equals(version)) { 444 | return release; 445 | } 446 | } 447 | if (type == ReleaseType.Latest || type == ReleaseType.Any) { 448 | if (release.version.major == version.major) { 449 | return release; 450 | } 451 | } 452 | } 453 | return null; 454 | } 455 | 456 | static create(github_releases: GitHubRelease[]): ReleaseDb { 457 | const R = new RegExp("(release-|prerelease-)?([0-9.]+)(-RC([0-9]+))?"); 458 | const releases = github_releases.map((gh_release) => { 459 | const m = gh_release.tag.match(R); 460 | if (m == null) { 461 | throw new SetupSdlError(`Invalid tag: ${gh_release.tag}`); 462 | } 463 | let prerelease: number | null = null; 464 | if (m[1] != null && m[1] != "release-") { 465 | prerelease = 1; 466 | } else if (m[3] != null && m[4] != null) { 467 | prerelease = Number(m[4]) + 1; 468 | } 469 | const version = m[2]; 470 | return new SdlRelease(new Version(version), prerelease, gh_release.tag); 471 | }); 472 | releases.sort((release1, release2) => { 473 | return release1.compare(release2); 474 | }); 475 | 476 | return new ReleaseDb(releases); 477 | } 478 | } 479 | 480 | class Release { 481 | version: Version; 482 | prerelease: number | null; 483 | tag: string; 484 | 485 | constructor(version: Version, prerelease: number | null, tag: string) { 486 | this.version = version; 487 | this.prerelease = prerelease; 488 | this.tag = tag; 489 | } 490 | 491 | compare(other: Release): number { 492 | const cmp = this.version.compare(other.version); 493 | if (cmp != 0) { 494 | return cmp; 495 | } 496 | if (this.prerelease != null && other.prerelease != null) { 497 | return Number(other.prerelease) - Number(this.prerelease); 498 | } 499 | if (this.prerelease == null && other.prerelease == null) { 500 | return 0; 501 | } 502 | if (this.prerelease != null) { 503 | return 1; 504 | } 505 | return -1; 506 | } 507 | 508 | equals(other: Release): boolean { 509 | return this.compare(other) == 0; 510 | } 511 | 512 | toString(): string { 513 | return ``; 514 | } 515 | } 516 | 517 | export class SdlRelease extends Release {} 518 | 519 | export type ParsedVersion = { 520 | version: Version | string; 521 | type: ReleaseType; 522 | }; 523 | 524 | export function parse_version_string( 525 | version_request: string, 526 | discarded_prefix: string | undefined, 527 | ): ParsedVersion { 528 | const ANY_SUFFIX = "-any"; 529 | const HEAD_SUFFIX = "-head"; 530 | const LATEST_SUFFIX = "-latest"; 531 | 532 | let version: Version; 533 | let version_type: ReleaseType; 534 | 535 | let stripped_version_request = version_request.toLowerCase(); 536 | if ( 537 | discarded_prefix && 538 | stripped_version_request.startsWith(discarded_prefix) 539 | ) { 540 | stripped_version_request = stripped_version_request.substring( 541 | discarded_prefix.length, 542 | ); 543 | } 544 | 545 | try { 546 | if (stripped_version_request.endsWith(ANY_SUFFIX)) { 547 | version_type = ReleaseType.Any; 548 | const version_str = stripped_version_request.substring( 549 | 0, 550 | stripped_version_request.length - ANY_SUFFIX.length, 551 | ); 552 | version = new Version({ 553 | major: Number(version_str), 554 | minor: 0, 555 | patch: 0, 556 | }); 557 | } else if (stripped_version_request.endsWith(HEAD_SUFFIX)) { 558 | version_type = ReleaseType.Head; 559 | const version_str = stripped_version_request.substring( 560 | 0, 561 | stripped_version_request.length - HEAD_SUFFIX.length, 562 | ); 563 | version = new Version({ 564 | major: Number(version_str), 565 | minor: 0, 566 | patch: 0, 567 | }); 568 | } else if (stripped_version_request.endsWith(LATEST_SUFFIX)) { 569 | version_type = ReleaseType.Latest; 570 | const version_str = stripped_version_request.substring( 571 | 0, 572 | stripped_version_request.length - LATEST_SUFFIX.length, 573 | ); 574 | version = new Version({ 575 | major: Number(version_str), 576 | minor: 0, 577 | patch: 0, 578 | }); 579 | } else { 580 | version_type = ReleaseType.Exact; 581 | const version_str = stripped_version_request; 582 | version = new Version(version_str); 583 | } 584 | return { version: version, type: version_type }; 585 | } catch { 586 | return { version: version_request, type: ReleaseType.Commit }; 587 | } 588 | } 589 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "outDir": "./build", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "esModuleInterop": true, 10 | "noEmitOnError": true, 11 | "types": ["node"] 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "**/*.test.ts"] 15 | } 16 | --------------------------------------------------------------------------------