feat: initial commit

This commit is contained in:
Joanna May 2023-01-30 18:32:27 -06:00
commit e128ba100d
24 changed files with 78314 additions and 0 deletions

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
dist/
lib/
node_modules/
jest.config.js

85
.eslintrc.json Normal file
View File

@ -0,0 +1,85 @@
{
"plugins": [
"jest",
"@typescript-eslint"
],
"extends": [
"plugin:github/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"i18n-text/no-en": "off",
"eslint-comments/no-use": "off",
"import/no-namespace": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"vars": "all",
"args": "none"
}
],
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
"accessibility": "no-public"
}
],
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-comment": "error",
"camelcase": "off",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/explicit-function-return-type": [
"error",
{
"allowExpressions": true
}
],
"@typescript-eslint/func-call-spacing": [
"error",
"never"
],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"semi": "off",
"@typescript-eslint/semi": [
"error",
"never"
],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unbound-method": "error"
},
"env": {
"node": true,
"es6": true,
"jest/globals": true
},
"globals": {
"NodeJS": true
}
}

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
dist/** -diff linguist-generated=true

11
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
- package-ecosystem: npm
directory: /
schedule:
interval: monthly

53
.github/workflows/check-dist.yml vendored Normal file
View File

@ -0,0 +1,53 @@
# `dist/index.js` is a special file in Actions.
# When you reference an action with `uses:` in a workflow,
# `index.js` is the code that will run.
# For our project, we generate this file through a build process from other source files.
# We need to make sure the checked-in `index.js` actually matches what we expect it to be.
name: Check dist/
on:
push:
branches:
- main
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
workflow_dispatch:
jobs:
check-dist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set Node.js 16.x
uses: actions/setup-node@v3.5.1
with:
node-version: 16.x
- name: Install dependencies
run: npm ci
- name: Rebuild the dist/ directory
run: |
npm run build
npm run package
- name: Compare the expected and actual dist/ directories
run: |
if [ "$(git diff --ignore-all-space --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then
echo "Detected uncommitted changes after build. See status below:"
git diff
exit 1
fi
id: diff
# If index.js was different than expected, upload the expected version as an artifact
- uses: actions/upload-artifact@v2
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
with:
name: dist
path: dist/

45
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: 'build-test'
on: # rebuild any PRs and main branch changes
pull_request:
push:
branches:
- main
- 'releases/*'
jobs:
tests:
name: 🧪 Test on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
# Don't cancel other OS runners if one fails.
fail-fast: false
matrix:
# Put the operating systems you want to run on here.
os: [ubuntu-latest, macos-latest, windows-latest]
env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_NOLOGO: true
defaults:
run:
# Use bash shells on all platforms.
shell: bash
steps:
- uses: actions/checkout@v3
name: 🧾 Checkout
- uses: actions/setup-dotnet@v3
name: 💽 Setup .NET SDK
with:
dotnet-version: '6.0.x'
- uses: ./
name: 🤖 Setup Godot
with:
# Version must include major, minor, and patch, and be >= 4.0.0
# Pre-release label is optional.
version: 4.0.0-beta16
- name: 🔬 Verify Setup
run: |
dotnet --version
godot --version

99
.gitignore vendored Normal file
View File

@ -0,0 +1,99 @@
# Dependency directory
node_modules
# Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# OS metadata
.DS_Store
Thumbs.db
# Ignore built ts files
__tests__/runner/*
lib/**/*

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
dist/
lib/
node_modules/

10
.prettierrc.json Normal file
View File

@ -0,0 +1,10 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": false,
"arrowParens": "avoid"
}

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2023 Chickensoft Games and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

70
README.md Normal file
View File

@ -0,0 +1,70 @@
# Setup Godot
Setup Godot for headless use with macOS, Windows, and Linux CI/CD runners.
- ✅ Godot 4 Only.
- ✅ Setup and run Godot on the OS you are developing for.
- ✅ Caches Godot 4 installation for speedier workflows.
- ✅ Adds environment variables (`GODOT4`, `GODOT`) to the system path.
- ✅ Installs Godot on the runner — do whatever you want with it afterwards!
> **Godot 3.x and below are not supported.**
## Usage
Example workflow:
```yaml
name: 🚥 Status Checks
on: push
jobs:
tests:
name: 👀 Evaluate on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
# Don't cancel other OS runners if one fails.
fail-fast: false
matrix:
# Put the operating systems you want to run on here.
os: [ubuntu-latest, macos-latest, windows-latest]
env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_NOLOGO: true
defaults:
run:
# Use bash shells on all platforms.
shell: bash
steps:
- uses: actions/checkout@v3
name: 🧾 Checkout
- uses: actions/setup-dotnet@v3
name: 💽 Setup .NET SDK
with:
# Use the .NET SDK from global.json in the root of the repository.
global-json-file: global.json
- name: 📦 Restore Dependencies
run: dotnet restore
- uses: chickensoft-games/setup-godot
name: 🤖 Setup Godot
with:
# Version must include major, minor, and patch, and be >= 4.0.0
# Pre-release label is optional.
version: 4.0.0-beta16
- name: 🔬 Verify Setup
run: |
dotnet --version
godot --version
- name: 🧑‍🔬 Generate .NET Bindings
run: godot --headless --build-solutions --quit || exit 0
- name: 🦺 Build Projects
run: dotnet build
# Do whatever you want!
```

75
__tests__/main.test.ts Normal file
View File

@ -0,0 +1,75 @@
import {describe, expect, test} from '@jest/globals'
import {getGodotUrl, getPlatform, parseVersion} from '../src/utils'
describe('parseVersion', () => {
test('parses valid godot versions', () => {
expect(parseVersion('3.5.2')).toEqual({
major: '3',
minor: '5',
patch: '2',
label: ''
})
expect(parseVersion('4.0.0-beta1')).toEqual({
major: '4',
minor: '0',
patch: '0',
label: 'beta1'
})
expect(parseVersion('4.0.0-beta.16')).toEqual({
major: '4',
minor: '0',
patch: '0',
label: 'beta.16'
})
})
})
describe('getGodotUrl', () => {
test('4.0.0-beta1', () => {
expect(getGodotUrl('4.0.0-beta1', getPlatform('linux'))).toEqual(
'https://downloads.tuxfamily.org/godotengine/4.0/beta1/mono/Godot_v4.0-beta1_mono_linux_x86_64.zip'
)
expect(getGodotUrl('4.0.0-beta1', getPlatform('win32'))).toEqual(
'https://downloads.tuxfamily.org/godotengine/4.0/beta1/mono/Godot_v4.0-beta1_mono_win64.zip'
)
expect(getGodotUrl('4.0.0-beta1', getPlatform('darwin'))).toEqual(
'https://downloads.tuxfamily.org/godotengine/4.0/beta1/mono/Godot_v4.0-beta1_mono_macos.universal.zip'
)
})
test('4.0.0-beta.16', () => {
expect(getGodotUrl('4.0.0-beta.16', getPlatform('linux'))).toEqual(
'https://downloads.tuxfamily.org/godotengine/4.0/beta16/mono/Godot_v4.0-beta16_mono_linux_x86_64.zip'
)
expect(getGodotUrl('4.0.0-beta.16', getPlatform('win32'))).toEqual(
'https://downloads.tuxfamily.org/godotengine/4.0/beta16/mono/Godot_v4.0-beta16_mono_win64.zip'
)
expect(getGodotUrl('4.0.0-beta.16', getPlatform('darwin'))).toEqual(
'https://downloads.tuxfamily.org/godotengine/4.0/beta16/mono/Godot_v4.0-beta16_mono_macos.universal.zip'
)
})
test('4.0.0-beta8', () => {
expect(getGodotUrl('4.0.0-beta8', getPlatform('linux'))).toEqual(
'https://downloads.tuxfamily.org/godotengine/4.0/beta8/mono/Godot_v4.0-beta8_mono_linux_x86_64.zip'
)
expect(getGodotUrl('4.0.0-beta8', getPlatform('win32'))).toEqual(
'https://downloads.tuxfamily.org/godotengine/4.0/beta8/mono/Godot_v4.0-beta8_mono_win64.zip'
)
expect(getGodotUrl('4.0.0-beta8', getPlatform('darwin'))).toEqual(
'https://downloads.tuxfamily.org/godotengine/4.0/beta8/mono/Godot_v4.0-beta8_mono_macos.universal.zip'
)
})
test('4.0.0', () => {
expect(getGodotUrl('4.0.0', getPlatform('linux'))).toEqual(
'https://downloads.tuxfamily.org/godotengine/4.0/mono/Godot_v4.0_mono_linux_x86_64.zip'
)
expect(getGodotUrl('4.0.0', getPlatform('win32'))).toEqual(
'https://downloads.tuxfamily.org/godotengine/4.0/mono/Godot_v4.0_mono_win64.zip'
)
expect(getGodotUrl('4.0.0', getPlatform('darwin'))).toEqual(
'https://downloads.tuxfamily.org/godotengine/4.0/mono/Godot_v4.0_mono_macos.universal.zip'
)
})
})

38
action.yml Normal file
View File

@ -0,0 +1,38 @@
name: 'Setup Godot Action'
description: >-
Setup Godot for headless use with macOS, Windows, and Linux CI/CD runners.
author: 'Chickensoft'
branding:
icon: 'hard-drive'
color: 'white'
inputs:
version:
description: >-
Godot 4 version: e.g., 4.0.0-beta1, 4.0.0-beta.16, 4.0.0, etc. Must
include major, minor, and patch (additional pre-release label is
optional).
required: true
path:
description: >-
Path to install Godot to, relative to the current working directory of
the action.
default: 'godot'
downloads-path:
description: >-
Path to download Godot to, relative to the current working directory of
the action.
default: 'downloads'
bin-path:
description: >-
Path for binaries to be installed to, relative to the current working
directory of the action. This is the path that will be added to the
system path.
default: 'bin'
godot-sharp-release:
description: >-
Whether to use the release or debug version of GodotSharp.dll. The
appropriate version will be symlinked in bin-path.
default: 'false'
runs:
using: 'node16'
main: 'dist/index.js'

7
cspell.json Normal file
View File

@ -0,0 +1,7 @@
{
"words": [
"Chickensoft",
"NOLOGO",
"OPTOUT"
]
}

64321
dist/index.js generated vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/index.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

1164
dist/licenses.txt generated vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/sourcemap-register.js generated vendored Normal file

File diff suppressed because one or more lines are too long

9
jest.config.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}

11835
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "setup-godot",
"version": "0.0.0",
"private": true,
"description": "Setup Godot for headless use with macOS, Windows, and Linux runners.",
"main": "lib/main.js",
"scripts": {
"build": "tsc",
"format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'",
"lint": "eslint src/**/*.ts",
"package": "ncc build --source-map --license licenses.txt",
"package-local": "export NODE_OPTIONS=--openssl-legacy-provider; ncc build --source-map --license licenses.txt",
"test": "jest",
"all": "npm run build && npm run format && npm run lint && npm run package && npm test",
"all-local": "npm run build && npm run format && npm run lint && npm run package-local && npm test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/chickensoft-games/setup-godot.git"
},
"keywords": [
"actions",
"node",
"setup"
],
"author": "",
"license": "MIT",
"dependencies": {
"@actions/cache": "^3.1.2",
"@actions/core": "^1.10.0",
"@actions/tool-cache": "^2.0.1"
},
"devDependencies": {
"@types/node": "^18.11.18",
"@typescript-eslint/parser": "^5.49.0",
"@vercel/ncc": "^0.36.1",
"eslint": "^8.33.0",
"eslint-plugin-github": "^4.6.0",
"eslint-plugin-jest": "^27.2.1",
"jest": "^29.4.1",
"js-yaml": "^4.1.0",
"prettier": "^2.8.3",
"ts-jest": "^29.0.5",
"typescript": "^4.9.4"
}
}

187
src/main.ts Normal file
View File

@ -0,0 +1,187 @@
import * as cache from '@actions/cache'
import * as core from '@actions/core'
import * as toolsCache from '@actions/tool-cache'
import * as fs from 'fs'
import * as os from 'os'
import path from 'path'
import {
findExecutablesRecursively,
getGodotFilenameFromVersionString,
getGodotUrl,
getPlatform,
Platform
} from './utils'
async function run(platform: Platform | undefined = undefined): Promise<void> {
platform = platform ?? getPlatform(process.platform)
// Get action inputs
const pathRelative = core.getInput('path').replace(/\s/g, '')
const downloadsRelativePath = core
.getInput('downloads-path')
.replace(/\s/g, '')
const version = core.getInput('version').replace(/\s/g, '')
const binRelativePath = core.getInput('bin-path').replace(/\s/g, '')
const godotSharpRelease = core.getBooleanInput('godot-sharp-release')
// Compute derived information
const userDir = os.homedir()
const downloadsDir = path.join(userDir, downloadsRelativePath)
const installationDir = path.join(userDir, pathRelative)
const versionName = getGodotFilenameFromVersionString(version, platform)
const godotUrl = getGodotUrl(version, platform)
const godotDownloadPath = path.join(downloadsDir, `${versionName}.zip`)
const godotInstallationPath = platform.getUnzippedPath(
installationDir,
versionName
)
const binDir = path.join(userDir, binRelativePath)
// Log values
core.startGroup('🤖 Godot Action Inputs')
core.info(`🤖 Godot version: ${version}`)
core.info(`🤖 Godot version name: ${versionName}`)
core.info(`🤖 Godot download url: ${godotUrl}`)
core.info(`🧑‍💼 User directory: ${userDir}`)
core.info(`🌏 Downloads directory: ${downloadsDir}`)
core.info(`📥 Godot download path: ${godotDownloadPath}`)
core.info(`📦 Godot installation directory: ${installationDir}`)
core.info(`🤖 Godot installation path: ${godotInstallationPath}`)
core.info(`📂 Bin directory: ${binDir}`)
core.info(`🤖 GodotSharp release: ${godotSharpRelease}`)
core.endGroup()
try {
// Ensure paths we are using exist.
core.startGroup(`📂 Ensuring working directories exist...`)
fs.mkdirSync(downloadsDir, {recursive: true})
fs.mkdirSync(installationDir, {recursive: true})
fs.mkdirSync(binDir, {recursive: true})
core.info(`✅ Working directories exist`)
core.endGroup()
// See if Godot is already installed.
core.startGroup(`🤔 Checking if Godot is already in cache...`)
const cached = await cache.restoreCache([godotInstallationPath], godotUrl)
let executables: string[]
if (!cached) {
// Download Godot
core.info(`🙃 Previous Godot download not found in cache`)
core.endGroup()
core.startGroup(`📥 Downloading Godot to ${godotDownloadPath}...`)
const godotDownloadedPath = await toolsCache.downloadTool(
godotUrl,
godotDownloadPath
)
core.info(`✅ Godot downloaded to ${godotDownloadedPath}`)
core.endGroup()
// Extract Godot
core.startGroup(`📦 Extracting Godot to ${installationDir}...`)
const godotExtractedPath = await toolsCache.extractZip(
godotDownloadedPath,
installationDir
)
core.info(`✅ Godot extracted to ${godotExtractedPath}`)
core.endGroup()
// Show extracted files recursively and list executables.
core.startGroup(`📄 Showing extracted files recursively...`)
executables = await findExecutablesRecursively(
platform,
installationDir,
''
)
core.info(`✅ Files shown`)
core.endGroup()
// Save extracted Godot contents to cache
core.startGroup(`💾 Saving extracted Godot download to cache...`)
await cache.saveCache([godotInstallationPath], godotUrl)
core.info(`✅ Godot saved to cache`)
core.endGroup()
} else {
core.info(`🎉 Previous Godot download found in cache!`)
core.endGroup()
core.startGroup(`📄 Showing cached files recursively...`)
executables = await findExecutablesRecursively(
platform,
installationDir,
''
)
core.info(`✅ Files shown`)
core.endGroup()
}
core.startGroup(`🚀 Executables:`)
for (const executable of executables) {
core.info(` 🚀 ${executable}`)
}
core.info(`✅ Executables shown`)
core.endGroup()
const godotExecutable = executables.find(exe =>
platform!.isGodotExecutable(path.basename(exe))
)
const godotSharp = executables.find(exe => {
const file = exe.toLowerCase()
return (
file.endsWith('godotsharp.dll') &&
(godotSharpRelease ? file.includes('release') : file.includes('debug'))
)
})!
if (!godotExecutable) {
throw new Error('🚨 No Godot executable found!')
}
if (!godotSharp) {
throw new Error('🚨 No GodotSharp.dll found!')
}
core.startGroup(`🚀 Resolve Godot Executables:`)
core.info(`🚀 Godot executable found at ${godotExecutable}`)
core.info(`🚀 GodotSharp.dll found at ${godotSharp}`)
core.endGroup()
// Add bin directory to PATH
core.startGroup(`🔦 Update PATH...`)
core.addPath(binDir)
core.info(`🔦 Added Bin Directory to PATH: ${binDir}`)
// Add path containing GodotSharp.dll to PATH
core.endGroup()
// Create symlink to Godot executable
const godotAlias = path.join(binDir, 'godot')
const godotSharpDirAlias = path.join(binDir, 'GodotSharp')
core.startGroup(`🔗 Creating symlinks to executables...`)
fs.linkSync(godotExecutable, godotAlias)
core.info(`✅ Symlink to Godot created`)
// Create symlink to GodotSharp directory
const godotSharpDir = path.join(path.dirname(godotSharp), '../..')
// fs.mkdirSync(godotSharpDirAlias, {recursive: true})
fs.symlinkSync(godotSharpDir, godotSharpDirAlias)
core.info(`✅ Symlink to GodotSharp created at ${godotSharpDirAlias}`)
core.endGroup()
// Add environment variables
core.startGroup(`🔧 Adding environment variables...`)
core.exportVariable('GODOT', godotAlias)
core.info(` GODOT=${godotAlias}`)
core.exportVariable('GODOT4', godotAlias)
core.info(` GODOT4=${godotAlias}`)
core.info(`✅ Environment variables added`)
core.endGroup()
core.info(`✅ Finished!`)
} catch (error) {
const message = `${error}`
core.setFailed(message)
}
}
run()

211
src/utils.ts Normal file
View File

@ -0,0 +1,211 @@
import * as core from '@actions/core'
import * as fs from 'fs'
import path from 'path'
export interface Platform {
/** Godot installation filename suffix. */
godotFilenameSuffix: string
/**
* Returns true if the given path is most likely the Godot executable for
* the platform.
* @param basename File basename to check.
*/
isGodotExecutable(basename: string): boolean
/**
* Returns the path to the unzipped file for the platform.
* @param installationDir Installation directory.
* @param versionName Version name.
*/
getUnzippedPath(installationDir: string, versionName: string): string
}
export class Linux implements Platform {
godotFilenameSuffix = '_mono_linux_x86_64'
isGodotExecutable(basename: string): boolean {
return basename.toLowerCase().endsWith('x86_64')
}
getUnzippedPath(installationDir: string, versionName: string): string {
return path.join(installationDir, versionName)
}
}
export class Windows implements Platform {
godotFilenameSuffix = '_mono_win64'
isGodotExecutable(basename: string): boolean {
return basename.toLowerCase().endsWith('_win64.exe')
}
getUnzippedPath(installationDir: string, versionName: string): string {
return path.join(installationDir, versionName)
}
}
export class MacOS implements Platform {
godotFilenameSuffix = '_mono_macos.universal'
isGodotExecutable(basename: string): boolean {
return basename.toLowerCase() === 'godot'
}
getUnzippedPath(installationDir: string, versionName: string): string {
return path.join(installationDir, 'Godot_mono.app')
}
}
/** Semantic version representation */
interface SemanticVersion {
/** Version major number */
major: string
/** Version minor number */
minor: string
/** Version patch number */
patch: string
/** Pre-release label (e.g., `beta.16`) */
label: string
}
/** Godot download url prefix. */
const GODOT_URL_PREFIX = 'https://downloads.tuxfamily.org/godotengine/'
/** Godot filename prefix. */
const GODOT_FILENAME_PREFIX = 'Godot_v'
/**
* Official semantic version regex.
* See https://semver.org
*/
const SEMANTIC_VERSION_REGEX =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
export function parseVersion(version: string): SemanticVersion {
const match = version.match(SEMANTIC_VERSION_REGEX)
if (match === null) {
throw new Error(`⛔️ Invalid version: ${version}`)
}
const major = match[1] || ''
const minor = match[2] || ''
const patch = match[3] || ''
const label = match[4] || ''
return {major, minor, patch, label}
}
/**
* Returns the Godot download url for the given version and platform.
* @param versionString Version string.
* @param platform Current platform instance.
* @returns Godot binary download url.
*/
export function getGodotUrl(versionString: string, platform: Platform): string {
const version = parseVersion(versionString)
const major = version.major
const minor = version.minor
const patch = version.patch
const label = version.label.replace('.', '')
const filename = getGodotFilename(version, platform)
let url = `${GODOT_URL_PREFIX + major}.${minor}`
if (patch !== '' && patch !== '0') {
url += `.${patch}`
}
url += '/'
if (label !== '') {
url += `${label}/`
}
url += `mono/${filename}.zip`
return url
}
export function getGodotFilename(
version: SemanticVersion,
platform: Platform
): string {
const major = version.major
const minor = version.minor
const patch = version.patch
const label = version.label.replace('.', '')
let filename = GODOT_FILENAME_PREFIX + major
if (minor !== '') {
filename += `.${minor}`
}
if (patch !== '' && patch !== '0') {
filename += `.${patch}`
}
if (label !== '') {
filename += `-${label}`
}
return filename + platform.godotFilenameSuffix
}
export function getGodotFilenameFromVersionString(
versionString: string,
platform: Platform
): string {
return getGodotFilename(parseVersion(versionString), platform)
}
export function getPlatform(processPlatform: NodeJS.Platform): Platform {
switch (processPlatform) {
case 'linux':
core.info('🐧 Running on Linux')
return new Linux()
case 'win32':
core.info('⧉ Running on Windows')
return new Windows()
case 'darwin':
core.info('🍏 Running on macOS')
return new MacOS()
default:
throw new Error(`⛔️ Unrecognized platform: ${process.platform}`)
}
}
export async function findExecutablesRecursively(
platform: Platform,
dir: string,
indent: string
): Promise<string[]> {
core.info(`${indent}📁 ${dir}`)
let executables: string[] = []
const files = await fs.promises.readdir(dir, {withFileTypes: true})
for (const file of files) {
const filePath = path.join(dir, file.name)
if (file.isDirectory()) {
const additionalExecutables = await findExecutablesRecursively(
platform,
filePath,
`${indent} `
)
executables = executables.concat(additionalExecutables)
} else {
// Test if file is executable. GodotSharp.dll is always considered an
// executable.
let isExecutable = file.name === 'GodotSharp.dll' ? true : false
if (!isExecutable) {
if (platform instanceof Windows) {
// fs.constants.X_OK doesn't seem to work on Windows.
// Resort to checking the file extension.
if (file.name.toLowerCase().endsWith('.exe')) {
isExecutable = true
}
} else {
try {
fs.accessSync(filePath, fs.constants.X_OK)
isExecutable = true
} catch (error) {
// File is not executable.
}
}
}
if (isExecutable) {
core.info(`${indent} 🚀 ${file.name}`)
executables.push(filePath)
} else {
core.info(`${indent} 📄 ${file.name}`)
}
}
}
return executables
}

15
tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"outDir": "./lib", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
},
"exclude": [
"node_modules",
"**/*.test.ts"
]
}