├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE │ ├── bug_fix.md │ ├── issue_resolving.md │ ├── new_feature.md │ └── trivial_fix.md ├── actions │ └── check-version │ │ ├── .gitignore │ │ ├── LICENSE.txt │ │ ├── action.yml │ │ ├── eslint.config.mjs │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── src │ │ └── index.ts │ │ └── tsconfig.json └── workflows │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── action.yml ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── script └── release ├── src ├── action_error.ts ├── commands.ts ├── get_installer.ts ├── index.ts ├── installer │ ├── build_installer.ts │ ├── linux_neovim_build_installer.ts │ ├── linux_neovim_releases_installer.ts │ ├── linux_vim_releases_installer.ts │ ├── macos_neovim_build_installer.ts │ ├── macos_neovim_releases_installer.ts │ ├── macvim_build_installer.ts │ ├── macvim_releases_installer.ts │ ├── neovim_build_installer.ts │ ├── neovim_releases_installer.ts │ ├── releases_installer.ts │ ├── semver_releases_installer.ts │ ├── unix_vim_build_installer.ts │ ├── vim_build_installer.ts │ ├── windows_neovim_build_installer.ts │ ├── windows_neovim_releases_installer.ts │ ├── windows_vim_build_installer.ts │ └── windows_vim_releases_installer.ts ├── interfaces.ts ├── patch.ts └── temp.ts └── tsconfig.json /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at @thinca. All complaints will be 59 | reviewed and investigated and will result in a response that is deemed 60 | necessary and appropriate to the circumstances. The project team is obligated 61 | to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for taking your time for this project! 4 | 5 | First, see [Code of Conduct](./CODE_OF_CONDUCT.md). 6 | 7 | 8 | ## Issues 9 | 10 | There are some templates. 11 | Please choose a template and fill them. 12 | 13 | When you cannot find an appropriate template, you do not have to use it. 14 | 15 | 16 | ### About `question` 17 | 18 | Please check [past `question` issues](../../../issues?q=label%3Aquestion) before you ask. 19 | 20 | 21 | ## Pull Requests 22 | 23 | There are some templates. 24 | Please choose a template and fill them. 25 | 26 | Please consider opening a issue before opening a PR when your proposal is big. 27 | 28 | 29 | ### About Git Repository 30 | 31 | #### Branches 32 | 33 | There are two important branches. 34 | 35 | - `develop` branch 36 | - A branch for development. 37 | - Please send a Pull Request to this if this exists. 38 | - `master` branch 39 | - A branch for release. 40 | - Do not send a Pull Request to this branch. 41 | 42 | 43 | #### Commits 44 | 45 | Keep clean. When you pushed dirty commits, please organize before merge. 46 | 47 | 48 | #### Force Push 49 | 50 | You can use force pushing freely. GitHub shows a link to see the difference between before force pushing and after force pushing. 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | ### Describe the bug 12 | 13 | 14 | ### How to reproduce the problem 15 | 16 | 17 | ### Expected behavior 18 | 19 | 20 | ### Actual behavior 21 | 22 | 23 | ### Screenshots (If possible) 24 | 25 | 26 | ### Additional context (If any) 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | ### Is your feature request related to a problem? 12 | 13 | 14 | ### Describe the solution you'd like 15 | 16 | 17 | ### Additional context 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Any questions about usage, features, or others 4 | title: '' 5 | labels: 'question' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | You can choose Pull Request template. (Select `Preview` tab first) 2 | 3 | - [Issue resolving](?template=issue_resolving.md&expand=1) 4 | - Resolve an existing issue 5 | - [Bug fix](?template=bug_fix.md&labels=bug&expand=1) 6 | - Fix a bug 7 | - [Trivial fix](?template=trivial_fix.md&labels=trivial&expand=1) 8 | - A small improvement fixing a typo, coding style, default configuration, and so on 9 | - [New feature](?template=new_feature.md&labels=enhancement&expand=1) 10 | - Propose a new feature to improve the project 11 | 12 | Or, remove all text and describe about your Pull Request! 13 | 14 | (Note that DO NOT USE this(you look) template. This is a navigation page.) 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/bug_fix.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | ### Describe the bug 8 | 9 | 10 | ### How to reproduce the problem 11 | 12 | 13 | ### Expected behavior 14 | 15 | 16 | ### Actual behavior 17 | 18 | 19 | ### Screenshots (If possible) 20 | 21 | 22 | ### Additional context (If any) 23 | 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/issue_resolving.md: -------------------------------------------------------------------------------- 1 | 2 | This pull request closes #xxx 3 | 4 | ### How to resolve 5 | 6 | 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/new_feature.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Is your suggesting new feature related to a problem? 4 | 5 | 6 | ### Describe the solution you'd like 7 | 8 | 9 | ### Additional context 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/trivial_fix.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /.github/actions/check-version/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /action.js 3 | -------------------------------------------------------------------------------- /.github/actions/check-version/LICENSE.txt: -------------------------------------------------------------------------------- 1 | zlib License 2 | 3 | (C) 2020 thinca 4 | 5 | This software is provided 'as-is', without any express or implied 6 | warranty. In no event will the authors be held liable for any damages 7 | arising from the use of this software. 8 | 9 | Permission is granted to anyone to use this software for any purpose, 10 | including commercial applications, and to alter it and redistribute it 11 | freely, subject to the following restrictions: 12 | 13 | 1. The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would be 16 | appreciated but is not required. 17 | 2. Altered source versions must be plainly marked as such, and must not be 18 | misrepresented as being the original software. 19 | 3. This notice may not be removed or altered from any source distribution. 20 | -------------------------------------------------------------------------------- /.github/actions/check-version/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Check Vim version' 2 | description: 'Checking the version of installed Vim' 3 | author: 'thinca ' 4 | 5 | inputs: 6 | expected_vim_version: 7 | description: |- 8 | Version of Vim. 9 | required: true 10 | vim_type: 11 | description: |- 12 | Type of Vim. 13 | This is one of `vim`, `neovim`, or `macvim`. 14 | required: true 15 | gui: 16 | description: |- 17 | When this is `yes`, setups the GUI version. 18 | And `outputs.executable` points to GUI version of Vim. 19 | required: false 20 | arch: 21 | description: |- 22 | Architecture of Vim. 23 | This is either of `x86_64` or `x86`, enable when `vim_type` is `vim` on Windows. 24 | required: false 25 | default: 'x86_64' 26 | executable: 27 | description: |- 28 | The name of executable file. 29 | required: true 30 | executable_path: 31 | description: |- 32 | The full path of executable file. 33 | required: true 34 | use_executable_path: 35 | description: |- 36 | When this is `yes`, uses `executable_path` to execute Vim. 37 | This is for multiple install tests. 38 | required: false 39 | 40 | branding: 41 | icon: 'check' 42 | color: 'green' 43 | 44 | runs: 45 | using: 'node20' 46 | main: 'action.js' 47 | -------------------------------------------------------------------------------- /.github/actions/check-version/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import globals from "globals"; 3 | import tsParser from "@typescript-eslint/parser"; 4 | import path from "node:path"; 5 | import { fileURLToPath } from "node:url"; 6 | import js from "@eslint/js"; 7 | import { FlatCompat } from "@eslint/eslintrc"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all 15 | }); 16 | 17 | export default [...compat.extends( 18 | "eslint:recommended", 19 | "plugin:@typescript-eslint/eslint-recommended", 20 | "plugin:@typescript-eslint/recommended", 21 | ), { 22 | plugins: { 23 | "@typescript-eslint": typescriptEslint, 24 | }, 25 | 26 | languageOptions: { 27 | globals: { 28 | ...globals.node, 29 | Atomics: "readonly", 30 | SharedArrayBuffer: "readonly", 31 | }, 32 | 33 | parser: tsParser, 34 | ecmaVersion: 2018, 35 | sourceType: "module", 36 | }, 37 | 38 | rules: { 39 | indent: ["error", 2, { 40 | SwitchCase: 1, 41 | }], 42 | 43 | "linebreak-style": ["error", "unix"], 44 | quotes: ["error", "double"], 45 | semi: ["error", "always"], 46 | }, 47 | }]; 48 | -------------------------------------------------------------------------------- /.github/actions/check-version/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "action-check-vim-version", 3 | "description": "A GitHub Action for checking version of Vim", 4 | "version": "0.0.0", 5 | "author": "thinca ", 6 | "bugs": { 7 | "url": "https://github.com/thinca/setup-vim-action/issues" 8 | }, 9 | "dependencies": { 10 | "@actions/core": "^1.11.1" 11 | }, 12 | "devDependencies": { 13 | "@eslint/eslintrc": "^3.3.0", 14 | "@eslint/js": "^9.21.0", 15 | "@types/node": "^20.17.19", 16 | "@typescript-eslint/eslint-plugin": "^8.25.0", 17 | "@typescript-eslint/parser": "^8.25.0", 18 | "esbuild": "0.25.0", 19 | "eslint": "^9.21.0", 20 | "eslint-plugin-import": "^2.31.0", 21 | "globals": "^16.0.0", 22 | "typescript": "^5.7.3" 23 | }, 24 | "homepage": "https://github.com/thinca/setup-vim-action", 25 | "keywords": [ 26 | "ci", 27 | "github", 28 | "github-actions", 29 | "vim" 30 | ], 31 | "license": "Zlib", 32 | "main": "index.js", 33 | "private": true, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/thinca/setup-vim-action.git" 37 | }, 38 | "scripts": { 39 | "build": "esbuild src/index.ts --bundle --platform=node --outfile=action.js", 40 | "lint": "eslint '**/*.ts'", 41 | "test": "echo \"Error: no test specified\" && exit 1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/actions/check-version/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import * as cp from "child_process"; 3 | import * as fs from "fs"; 4 | import {promisify} from "util"; 5 | 6 | const execFile = promisify(cp.execFile); 7 | const readFile = promisify(fs.readFile); 8 | const writeFile = promisify(fs.writeFile); 9 | 10 | function versionOutputCmd(outFile: string): string { 11 | return [ 12 | `redir! > ${outFile}`, 13 | "version", 14 | "redir END", 15 | ].join(" | "); 16 | } 17 | 18 | function timeout(promise: Promise, timeoutMilliseconds: number): Promise { 19 | const timeoutPromise = new Promise( 20 | (_resolve, reject) => 21 | setTimeout( 22 | () => reject(new Error("Execution timeout")), 23 | timeoutMilliseconds 24 | ) 25 | ); 26 | return Promise.race([promise, timeoutPromise]); 27 | } 28 | 29 | async function retry(promiseMaker: () => Promise, tryCount: number): Promise { 30 | let rejected: unknown; 31 | while (0 < tryCount--) { 32 | try { 33 | return await promiseMaker(); 34 | } catch (e) { 35 | rejected = e; 36 | } 37 | } 38 | return Promise.reject(rejected); 39 | } 40 | 41 | // "v8.2.0012" -> ["8.2.12", null] 42 | // "v0.5.0" -> ["0.5", null] 43 | // "v0.5.0-404-g49cd750d6" -> ["v0.5.0", "49cd750d6"] 44 | // "v0.5.0-dev+1330-gd16e9d8ed" -> ["v0.5.0", "d16e9d8ed"] 45 | // "v0.5.0-dev-1330+gd16e9d8ed" -> ["v0.5.0", "d16e9d8ed"] 46 | // "v0.5.0-nightly" -> ["v0.5.0", "nightly"] 47 | // "v0.5.0-dev+nightly" -> ["v0.5.0", "nightly"] 48 | // "49cd750d6a72efc0571a89d7a874bbb01081227f" -> [null, "49cd750d6a72efc0571a89d7a874bbb01081227f"] 49 | function normalizeVersion(str: string): [string | null, string | null] { 50 | if (str.indexOf(".") < 0) { 51 | // Probably sha1. 52 | return [null, str]; 53 | } 54 | str = str.replace("dev+", ""); 55 | 56 | let semver: string | null = null; 57 | let sha1OrTag: string | null = null; 58 | 59 | const semverMatched = /^v?(\d+(?:\.\d+)*)/.exec(str); 60 | if (semverMatched) { 61 | semver = semverMatched[1]. 62 | replace(/(^|[^\d])0+(\d)/g, "$1$2"). 63 | replace(/(?:\.0)+$/, ""); 64 | } 65 | const sha1Matched = /^.*-\d+[-+]g([0-9a-f]{7,})(?:-\w+)?$/.exec(str); 66 | if (sha1Matched) { 67 | sha1OrTag = sha1Matched[1]; 68 | } else if (0 <= str.indexOf("-")) { 69 | const parts = str.split("-"); 70 | sha1OrTag = parts[parts.length - 1]; 71 | } 72 | 73 | return [semver, sha1OrTag]; 74 | } 75 | 76 | function extractVersionFromVersionOutput(verstionText: string): [string | null, string | null] { 77 | const lines = verstionText.trimStart().split(/\r?\n/); 78 | const majorMinorMatched = /VIM - Vi IMproved (\d+)\.(\d+)/.exec(lines[0]); 79 | if (majorMinorMatched) { 80 | const [major, minor] = majorMinorMatched.slice(1, 3).map((n) => parseInt(n)); 81 | let patch = 0; 82 | for (const line of lines.slice(1, 5)) { 83 | const patchMatched = /^Included patches: .*?(\d+)$/.exec(line); 84 | if (patchMatched) { 85 | patch = parseInt(patchMatched[1]); 86 | break; 87 | } 88 | } 89 | return normalizeVersion([major, minor, patch].join(".")); 90 | } else { 91 | const matched = /^NVIM (.*)/.exec(lines[0]); 92 | if (matched) { 93 | return normalizeVersion(matched[1]); 94 | } 95 | } 96 | return [null, null]; 97 | } 98 | 99 | const COMMAND_TIMEOUT = 5 * 1000; 100 | const RETRY_COUNT = 3; 101 | 102 | async function getCUIVersionOutput(executable: string): Promise { 103 | const {stdout} = await retry(() => timeout(execFile(executable, ["--version"]), COMMAND_TIMEOUT), RETRY_COUNT); 104 | return stdout; 105 | } 106 | 107 | async function getWindowsGUIVersionOutput(executable: string): Promise { 108 | // gVim on Windows shows version info from "--version" via GUI dialog, so we use other approach. 109 | const waitForRegisterSec = 2; 110 | const bat = [ 111 | `start /wait ${executable} -silent -register`, 112 | `ping -n ${waitForRegisterSec + 1} localhost > NUL`, 113 | `start /wait ${executable} -u NONE -c "${versionOutputCmd("version.txt")}" -c "qall!"`, 114 | ]; 115 | await writeFile("version.bat", bat.join("\n")); 116 | 117 | await retry(() => timeout(execFile("call", ["version.bat"], {shell: true}), COMMAND_TIMEOUT + waitForRegisterSec * 1000), RETRY_COUNT); 118 | 119 | return await readFile("version.txt", "utf8"); 120 | } 121 | 122 | async function getUnixGUIVersionOutput(executable: string): Promise { 123 | 124 | await retry(() => timeout(execFile(executable, ["--cmd", versionOutputCmd("version.txt"), "--cmd", "qall!"]), COMMAND_TIMEOUT), RETRY_COUNT); 125 | 126 | return await readFile("version.txt", "utf8"); 127 | } 128 | 129 | async function getGUIVersionOutput(vimType: string, executable: string): Promise { 130 | // XXX: MacVim with GUI cannot be supported. 131 | if (vimType === "neovim") { 132 | // GUI of Neovim is a wrapper for CUI version so we check just a CUI version 133 | return await getCUIVersionOutput("nvim"); 134 | } 135 | 136 | if (process.platform === "win32") { 137 | return await getWindowsGUIVersionOutput(executable); 138 | } 139 | 140 | return await getUnixGUIVersionOutput(executable); 141 | } 142 | 143 | async function getVersionOutput(vimType: string, isGUI: boolean, executable: string): Promise { 144 | if (isGUI) { 145 | return await getGUIVersionOutput(vimType, executable); 146 | } else { 147 | return await getCUIVersionOutput(executable); 148 | } 149 | } 150 | 151 | async function check(): Promise { 152 | const vimType = core.getInput("vim_type").toLowerCase(); 153 | const isGUI = core.getInput("gui") === "yes"; 154 | const executable = core.getInput("executable"); 155 | const expectedVimVersion = core.getInput("expected_vim_version"); 156 | 157 | const executablePath = core.getInput("executable_path"); 158 | if (!fs.existsSync(executablePath)) { 159 | throw new Error(`"executable_path" does not exist: ${executablePath}`); 160 | } 161 | 162 | const vimFile = core.getInput("use_executable_path") === "yes" ? executablePath : executable; 163 | 164 | core.info(`Expected Version: ${expectedVimVersion}`); 165 | 166 | const versionOutput = (await getVersionOutput(vimType, isGUI, vimFile)).trim(); 167 | 168 | const [actualSemverVersion, actualSha1Version] = extractVersionFromVersionOutput(versionOutput); 169 | 170 | core.info(`Actual Version: ${actualSemverVersion} ${actualSha1Version}`); 171 | core.info("-------"); 172 | core.info(versionOutput); 173 | core.info("-------"); 174 | 175 | if (process.platform === "win32" && vimType === "vim") { 176 | const actualArch = /(32|64)-bit/.exec(versionOutput)?.[0]; 177 | const expectedArch = core.getInput("arch").includes("64") ? "64-bit" : "32-bit"; 178 | if (expectedArch !== actualArch) { 179 | throw Error(`Installed Vim's architecture is wrong:\nexpected: ${expectedArch}\nactual: ${actualArch}`); 180 | } 181 | } 182 | 183 | const [expectedSemverVersion, expectedSha1Version] = normalizeVersion(expectedVimVersion); 184 | 185 | if (expectedSha1Version === "nightly") { 186 | return `Cannot check the version:\nexpected: ${expectedVimVersion}\nactual: ${actualSha1Version || actualSemverVersion}`; 187 | } 188 | 189 | if (expectedSha1Version != null && actualSha1Version != null) { 190 | if (vimType === "neovim") { 191 | if (expectedSha1Version.startsWith(actualSha1Version)) { 192 | return "Correct version installed"; 193 | } 194 | } else { 195 | return `Cannot check the version:\nexpected: ${expectedVimVersion}\nactual: ${actualSha1Version}`; 196 | } 197 | } else if (expectedSemverVersion === actualSemverVersion) { 198 | return "Correct version installed"; 199 | } 200 | 201 | throw Error(`Installed Vim's version is wrong:\nexpected: ${expectedVimVersion} (${expectedSemverVersion})\nactual: ${actualSemverVersion}`); 202 | } 203 | 204 | async function main(): Promise { 205 | core.info(await check()); 206 | } 207 | 208 | 209 | main().catch(e => { 210 | const message = e.message || JSON.stringify(e); 211 | core.setFailed(message); 212 | process.exit(1); 213 | }); 214 | -------------------------------------------------------------------------------- /.github/actions/check-version/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib", 4 | "module": "node16", 5 | "moduleResolution": "node16", 6 | "lib": ["ES2023"], 7 | "strict": true, 8 | "preserveConstEnums": true, 9 | "noImplicitAny": true, 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noEmitOnError": true, 15 | "strictNullChecks": true, 16 | "target": "es2022", 17 | "sourceMap": true, 18 | "esModuleInterop": true, 19 | "resolveJsonModule": true 20 | }, 21 | "include": [ 22 | "src/**/*.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: 'Lint' 2 | on: ['push', 'pull_request'] 3 | 4 | jobs: 5 | lint: 6 | name: 'Run ESLint' 7 | runs-on: 'ubuntu-latest' 8 | steps: 9 | - uses: 'actions/checkout@v4' 10 | - name: 'Cache node packages' 11 | uses: 'actions/cache@v4' 12 | with: 13 | path: '~/.npm' 14 | key: "${{ hashFiles('package-lock.json') }}-${{ hashFiles('.github/actions/check-version/package-lock.json') }}" 15 | - name: 'Run ESLint for setup-vim' 16 | run: | 17 | npm ci 18 | npm run --silent lint 19 | - name: 'Run ESLint for check-version' 20 | if: 'always()' 21 | working-directory: '.github/actions/check-version' 22 | run: | 23 | npm ci 24 | npm run --silent lint 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 'Release' 2 | on: 3 | workflow_run: 4 | workflows: ["Test"] 5 | types: 6 | - completed 7 | branches: 8 | - 'v[0-9]+.[0-9]+.[0-9]+\+src' 9 | 10 | jobs: 11 | release: 12 | name: 'Release' 13 | if: "github.event.workflow_run.conclusion == 'success'" 14 | runs-on: 'ubuntu-latest' 15 | steps: 16 | - uses: 'actions/checkout@v4' 17 | - name: 'Release a new version' 18 | run: | 19 | set -x 20 | 21 | GITHUB_SHA=${{ github.event.workflow_run.head_commit.id }} 22 | ORIGINAL_TAG=${{ github.event.workflow_run.head_branch }} 23 | PATCH_VER=${ORIGINAL_TAG%+src} 24 | MINOR_VER=${PATCH_VER%.*} 25 | MAJOR_VER=${MINOR_VER%.*} 26 | 27 | git fetch --no-tags --force origin tag ${ORIGINAL_TAG} 28 | git reset --hard ${ORIGINAL_TAG} 29 | 30 | SUBJECT="$(git tag --list "${ORIGINAL_TAG}" --format="%(contents:subject)")" 31 | SUBJECT_JSON=$(echo -n "${SUBJECT}" | jq --raw-input --slurp .) 32 | BODY="$(git tag --list "${ORIGINAL_TAG}" --format="%(contents:body)")" 33 | BODY_JSON=$(echo -n "${BODY}" | jq --raw-input --slurp .) 34 | 35 | npm ci 36 | npm run --silent build 37 | git switch --orphan release 38 | git checkout "${GITHUB_SHA}" -- action.yml README.md 39 | git add action.js 40 | git config user.email "$(git log -1 --format=format:%ce ${GITHUB_SHA})" 41 | git config user.name "$(git log -1 --format=format:%cn ${GITHUB_SHA})" 42 | git commit -C "${GITHUB_SHA}" 43 | git tag "${PATCH_VER}" 44 | git tag "${MINOR_VER}" 45 | git tag "${MAJOR_VER}" 46 | git push origin --force "${MAJOR_VER}" "${MINOR_VER}" "${PATCH_VER}" 47 | 48 | wget --method=POST \ 49 | --quiet --content-on-error \ 50 | --output-document=- \ 51 | --header 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ 52 | --header 'Content-Type: application/json' \ 53 | --body-data '{ 54 | "tag_name": "'"${PATCH_VER}"'", 55 | "name": '"${SUBJECT_JSON}"', 56 | "body": '"${BODY_JSON}"' 57 | }' \ 58 | 'https://api.github.com/repos/${{ github.repository }}/releases' 59 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'Test' 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '0 0 * * SAT' 7 | 8 | jobs: 9 | setup-actions: 10 | name: 'Setup actions' 11 | runs-on: 'ubuntu-latest' 12 | steps: 13 | - uses: 'actions/checkout@v4' 14 | - name: 'Cache node packages' 15 | uses: 'actions/cache@v4' 16 | with: 17 | path: '~/.npm' 18 | key: "${{ hashFiles('package-lock.json') }}-${{ hashFiles('.github/actions/check-version/package-lock.json') }}" 19 | - name: 'Build actions' 20 | run: | 21 | npm ci 22 | npm run --silent build 23 | mkdir -p ${{ github.workspace }}/actions/setup-vim 24 | cp action.js action.yml ${{ github.workspace }}/actions/setup-vim 25 | 26 | cd ${{ github.workspace }}/.github/actions/check-version 27 | npm ci 28 | npm run --silent build 29 | mkdir -p ${{ github.workspace }}/actions/check-version 30 | cp action.js action.yml ${{ github.workspace }}/actions/check-version 31 | - name: 'Upload actions as artifact' 32 | uses: 'actions/upload-artifact@v4' 33 | with: 34 | name: 'actions' 35 | path: 'actions' 36 | - name: 'Get GitHub GraphQL API rate limit' 37 | id: rate_limit 38 | uses: octokit/graphql-action@v2.x 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | with: 42 | query: | 43 | query { 44 | rateLimit { 45 | limit 46 | cost 47 | remaining 48 | resetAt 49 | } 50 | } 51 | - name: 'Show GitHub GraphQL API rate limit' 52 | run: echo "${{ steps.rate_limit.outputs.data }}" 53 | 54 | 55 | generate-matrix: 56 | name: 'Generate matrix' 57 | runs-on: 'ubuntu-latest' 58 | outputs: 59 | matrix: ${{ steps.generate.outputs.result }} 60 | steps: 61 | - name: 'Generate matrix' 62 | id: generate 63 | uses: 'actions/github-script@v7' 64 | with: 65 | script: | 66 | const matrix = [ 67 | ["Vim", "head"], 68 | ["Vim", "v9.1.0000"], 69 | ["Vim", "v9.0.0000"], 70 | ["Vim", "v8.2.0000"], 71 | ["Vim", "v8.1.0001"], 72 | ["Vim", "v8.0.0002"], 73 | ["Vim", "v7.4"], 74 | ["Neovim", "head"], 75 | ["Neovim", "v0.10.2"], 76 | ["Neovim", "v0.9.5"], 77 | ["Neovim", "v0.9.0"], 78 | ["Neovim", "v0.8.3"], 79 | ["MacVim", "head"], 80 | ["MacVim", "release-179"], // v9.1.0000 81 | ["MacVim", "snapshot-173"], // v9.0.0065 82 | ["MacVim", "snapshot-162"], // v8.2.319 83 | ].flatMap(([vim_type, vim_version]) => 84 | ["Linux", "MacOS", "Windows"].map(platform => ({ 85 | vim_type, 86 | vim_version, 87 | platform, 88 | })) 89 | ).flatMap((param) => 90 | ["GUI", "CUI"].map((gui_cui) => ({ 91 | ...param, 92 | gui_cui, 93 | })) 94 | ).flatMap((param) => 95 | ["DL", "AT", "BD"].map((dl_bd) => ({ 96 | ...param, 97 | dl_bd, 98 | })) 99 | ).flatMap((param) => 100 | ["x86_64"].map((arch) => ({ 101 | ...param, 102 | arch, 103 | })) 104 | ).concat( 105 | [ 106 | { 107 | vim_type: "Vim", 108 | vim_version: "head", 109 | platform: "Windows", 110 | gui_cui: "GUI", 111 | dl_bd: "BD", 112 | arch: "x86", 113 | }, 114 | ] 115 | ).filter(({ vim_type, vim_version, platform, gui_cui, dl_bd }) => { 116 | // DL Vim on MacOS is unavailable 117 | if (vim_type === "Vim" && platform === "MacOS" && dl_bd === "DL") { 118 | return false; 119 | } 120 | // Downloading Vim from AppImage is available from v8.1.1239. 121 | if (vim_type === "Vim" && platform === "Linux" && dl_bd === "DL" && 122 | (vim_version === "v8.1.0001" || vim_version === "v8.0.0002" || vim_version === "v7.4")) { 123 | return false; 124 | } 125 | // GUI version of Neovim is unavailable (also skip on Windows) 126 | if (vim_type === "Neovim" && gui_cui === "GUI") { 127 | return false; 128 | } 129 | // Build Neovim on Windows is not supported yet 130 | if (vim_type === "Neovim" && platform === "Windows" && dl_bd === "BD") { 131 | return false; 132 | } 133 | // GUI version of MacVim is unavailable 134 | if (platform === "MacOS" && gui_cui === "GUI") { 135 | return false; 136 | } 137 | // MacVim is available only on MacOS 138 | if (vim_type === "MacVim" && platform !== "MacOS") { 139 | return false; 140 | } 141 | return true; 142 | }).map(({ vim_type, vim_version, platform, gui_cui, dl_bd }) => ({ 143 | vim_type, 144 | vim_version, 145 | platform, 146 | gui_cui, 147 | dl_bd, 148 | os: { 149 | Linux: "ubuntu-latest", 150 | MacOS: "macos-latest", 151 | Windows: "windows-latest", 152 | }[platform], 153 | gui: gui_cui === "GUI" ? "yes" : "no", 154 | download: { 155 | DL: "always", 156 | AT: "available", 157 | BD: "never", 158 | }[dl_bd], 159 | })); 160 | console.log(JSON.stringify(matrix, null, 2)); 161 | return { include: matrix }; 162 | 163 | test: 164 | name: "${{ matrix.gui_cui }}/${{ matrix.dl_bd }}: ${{ matrix.vim_type }} ${{ matrix.vim_version }} on ${{ matrix.platform }}${{ matrix.arch == 'x86' && ' (x86)' || ''}}" 165 | strategy: 166 | matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }} 167 | fail-fast: false 168 | 169 | needs: 170 | - 'setup-actions' 171 | - 'generate-matrix' 172 | runs-on: '${{ matrix.os }}' 173 | timeout-minutes: 20 174 | defaults: 175 | run: 176 | shell: bash 177 | 178 | steps: 179 | - name: 'Show job info' 180 | run: | 181 | echo "Platform: ${{ matrix.platform }}" 182 | echo "OS: ${{ matrix.os }}" 183 | echo "Vim type: ${{ matrix.vim_type }}" 184 | echo "Vim version: ${{ matrix.vim_version }}" 185 | echo "GUI: ${{ matrix.gui }}" 186 | echo "Download: ${{ matrix.download }}" 187 | echo "Architecture: ${{ matrix.arch }}" 188 | echo "Cache key: 'test-${{ github.run_id }}-${{ github.run_attempt }}-${{ strategy.job-index }}'" 189 | - name: 'Download actions' 190 | uses: 'actions/download-artifact@v4' 191 | with: 192 | name: 'actions' 193 | path: 'actions' 194 | - name: 'Setup Vim' 195 | id: 'vim' 196 | uses: './actions/setup-vim' 197 | with: 198 | vim_version: '${{ matrix.vim_version }}' 199 | vim_type: '${{ matrix.vim_type }}' 200 | gui: '${{ matrix.gui }}' 201 | arch: '${{ matrix.arch }}' 202 | download: '${{ matrix.download }}' 203 | cache: 'test-${{ github.run_id }}-${{ github.run_attempt }}-${{ strategy.job-index }}' 204 | - name: 'Check Setup Vim is done without cache' 205 | if: "matrix.download == 'never'" 206 | run: | 207 | if [[ "${{ steps.vim.outputs.cache_hit }}" == "true" ]]; then 208 | echo "Cache should not hit, but hit." 209 | exit 1 210 | else 211 | echo "Cache did not hit as expected." 212 | fi 213 | - name: 'Check Vim version' 214 | uses: './actions/check-version' 215 | with: 216 | expected_vim_version: '${{ steps.vim.outputs.actual_vim_version }}' 217 | executable: '${{ steps.vim.outputs.executable }}' 218 | executable_path: '${{ steps.vim.outputs.executable_path }}' 219 | vim_type: '${{ matrix.vim_type }}' 220 | gui: '${{ matrix.gui }}' 221 | arch: '${{ matrix.arch }}' 222 | - name: 'Delete installed Vim to reinstall' 223 | if: "matrix.download == 'never'" 224 | run: | 225 | rm -fr "${{ steps.vim.outputs.install_path }}" 226 | - name: 'Setup Vim with cache' 227 | id: 'vim_with_cache' 228 | if: "matrix.download == 'never'" 229 | uses: './actions/setup-vim' 230 | with: 231 | vim_version: '${{ matrix.vim_version }}' 232 | vim_type: '${{ matrix.vim_type }}' 233 | gui: '${{ matrix.gui }}' 234 | download: '${{ matrix.download }}' 235 | cache: 'test-${{ github.run_id }}-${{ github.run_attempt }}-${{ strategy.job-index }}' 236 | - name: 'Check Setup Vim is done with cache' 237 | if: "matrix.download == 'never'" 238 | run: | 239 | if [[ "${{ steps.vim_with_cache.outputs.cache_hit }}" == "true" ]]; then 240 | echo "Cache hit!" 241 | else 242 | echo "Cache not hit..." 243 | exit 1 244 | fi 245 | 246 | multiple-test: 247 | name: 'Install multiple Vims on ${{ matrix.platform }}' 248 | strategy: 249 | matrix: 250 | platform: ['Linux', 'MacOS', 'Windows'] 251 | 252 | include: 253 | - platform: 'Linux' 254 | os: 'ubuntu-latest' 255 | - platform: 'MacOS' 256 | os: 'macos-latest' 257 | - platform: 'Windows' 258 | os: 'windows-latest' 259 | fail-fast: false 260 | 261 | needs: 'setup-actions' 262 | runs-on: '${{ matrix.os }}' 263 | defaults: 264 | run: 265 | shell: bash 266 | 267 | steps: 268 | - name: 'Download actions' 269 | uses: 'actions/download-artifact@v4' 270 | with: 271 | name: 'actions' 272 | path: 'actions' 273 | 274 | - name: 'Setup Vim head' 275 | id: 'vim_head' 276 | uses: './actions/setup-vim' 277 | with: 278 | vim_version: 'head' 279 | vim_type: 'Vim' 280 | - name: 'Setup Vim latest' 281 | id: 'vim_latest' 282 | uses: './actions/setup-vim' 283 | with: 284 | vim_version: 'v9.1.0000' 285 | vim_type: 'Vim' 286 | - name: 'Setup Neovim head' 287 | id: 'neovim_head' 288 | uses: './actions/setup-vim' 289 | with: 290 | vim_version: 'head' 291 | vim_type: 'Neovim' 292 | - name: 'Setup Neovim latest' 293 | id: 'neovim_latest' 294 | uses: './actions/setup-vim' 295 | with: 296 | vim_version: 'v0.10.2' 297 | vim_type: 'Neovim' 298 | - name: 'Setup MacVim head' 299 | if: "matrix.platform == 'MacOS'" 300 | id: 'macvim_head' 301 | uses: './actions/setup-vim' 302 | with: 303 | vim_version: 'head' 304 | vim_type: 'MacVim' 305 | - name: 'Setup MacVim latest' 306 | if: "matrix.platform == 'MacOS'" 307 | id: 'macvim_latest' 308 | uses: './actions/setup-vim' 309 | with: 310 | vim_version: 'release-179' 311 | vim_type: 'MacVim' 312 | 313 | - name: 'Check Vim head version' 314 | if: "always() && steps.vim_head.conclusion == 'success'" 315 | uses: './actions/check-version' 316 | with: 317 | expected_vim_version: '${{ steps.vim_head.outputs.actual_vim_version }}' 318 | executable: '${{ steps.vim_head.outputs.executable }}' 319 | executable_path: '${{ steps.vim_head.outputs.executable_path }}' 320 | vim_type: 'Vim' 321 | use_executable_path: 'yes' 322 | - name: 'Check Vim latest version' 323 | if: "always() && steps.vim_latest.conclusion == 'success'" 324 | uses: './actions/check-version' 325 | with: 326 | expected_vim_version: '${{ steps.vim_latest.outputs.actual_vim_version }}' 327 | executable: '${{ steps.vim_latest.outputs.executable }}' 328 | executable_path: '${{ steps.vim_latest.outputs.executable_path }}' 329 | vim_type: 'Vim' 330 | use_executable_path: 'yes' 331 | - name: 'Check Neovim head version' 332 | if: "always() && steps.neovim_head.conclusion == 'success'" 333 | uses: './actions/check-version' 334 | with: 335 | expected_vim_version: '${{ steps.neovim_head.outputs.actual_vim_version }}' 336 | executable: '${{ steps.neovim_head.outputs.executable }}' 337 | executable_path: '${{ steps.neovim_head.outputs.executable_path }}' 338 | vim_type: 'Neovim' 339 | use_executable_path: 'yes' 340 | - name: 'Check Neovim latest version' 341 | if: "always() && steps.neovim_latest.conclusion == 'success'" 342 | uses: './actions/check-version' 343 | with: 344 | expected_vim_version: '${{ steps.neovim_latest.outputs.actual_vim_version }}' 345 | executable: '${{ steps.neovim_latest.outputs.executable }}' 346 | executable_path: '${{ steps.neovim_latest.outputs.executable_path }}' 347 | vim_type: 'Neovim' 348 | use_executable_path: 'yes' 349 | - name: 'Check MacVim head version' 350 | if: "always() && steps.macvim_head.conclusion == 'success'" 351 | uses: './actions/check-version' 352 | with: 353 | expected_vim_version: '${{ steps.macvim_head.outputs.actual_vim_version }}' 354 | executable: '${{ steps.macvim_head.outputs.executable }}' 355 | executable_path: '${{ steps.macvim_head.outputs.executable_path }}' 356 | vim_type: 'MacVim' 357 | use_executable_path: 'yes' 358 | - name: 'Check MacVim latest version' 359 | if: "always() && steps.macvim_latest.conclusion == 'success'" 360 | uses: './actions/check-version' 361 | with: 362 | expected_vim_version: '${{ steps.macvim_latest.outputs.actual_vim_version }}' 363 | executable: '${{ steps.macvim_latest.outputs.executable }}' 364 | executable_path: '${{ steps.macvim_latest.outputs.executable_path }}' 365 | vim_type: 'MacVim' 366 | use_executable_path: 'yes' 367 | 368 | 369 | show-limit-after: 370 | name: 'Show GitHub API rate limit after test' 371 | needs: ['test', 'multiple-test'] 372 | runs-on: 'ubuntu-latest' 373 | timeout-minutes: 1 374 | if: 'always()' 375 | steps: 376 | - name: 'Get GitHub GraphQL API rate limit' 377 | id: rate_limit 378 | uses: octokit/graphql-action@v2.x 379 | env: 380 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 381 | with: 382 | query: | 383 | query { 384 | rateLimit { 385 | limit 386 | cost 387 | remaining 388 | resetAt 389 | } 390 | } 391 | - name: 'Show GitHub GraphQL API rate limit' 392 | run: echo "${{ steps.rate_limit.outputs.data }}" 393 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /action.js 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v2.1.2 - 2025-03-03 4 | 5 | - Fixed: Downloading latest Neovim. 6 | - Security: Update dependencies. 7 | - Update all dependencies by resetting the lock file. 8 | - This Action was previously failing when using a cache, but this has been fixed in this update. 9 | 10 | 11 | ## v2.1.1 - 2025-01-06 12 | 13 | - Fixed: Add support for downloading Vim on Ubuntu 24.04. 14 | - Fixed: Add a patch for building old Neovim on MacOS 15. 15 | 16 | 17 | ## v2.1.0 - 2024-12-11 18 | 19 | - Fixed: Remove unnecessary packages in MacOS Neovim build installer. 20 | - Fixed: Fix some problems with latest GitHub-hosted runners. 21 | - Add patches for older Vim in Windows and MacOS. 22 | - Security: Update dependencies. 23 | 24 | 25 | ## v2.0.3 - 2024-06-26 26 | 27 | - Security: Update dependencies. 28 | 29 | 30 | ## v2.0.2 - 2024-04-25 31 | 32 | - Security: Update dependencies. 33 | 34 | 35 | ## v2.0.1 - 2024-03-29 36 | 37 | - Fixed: Downloading Neovim in Mac. (Thanks [@mityu](https://github.com/mityu) [#14](https://github.com/thinca/action-setup-vim/pull/14)) 38 | - Security: Update dependencies. 39 | 40 | 41 | ## v2.0.0 - 2024-03-02 42 | 43 | - Breaking Change: Update Node 16 to Node 20 the actions. 44 | - Security: Update dependencies. 45 | 46 | 47 | ## v1.2.11 - 2023-08-01 48 | 49 | - Security: Update dependencies. 50 | 51 | 52 | ## v1.2.10 - 2023-05-02 53 | 54 | - Security: Update dependencies. 55 | 56 | 57 | ## v1.2.9 - 2023-01-12 58 | 59 | - Security: Update dependencies. 60 | 61 | 62 | ## v1.2.8 - 2022-12-28 63 | 64 | - Fixed: AppImage did not work on Ubuntu 22.04. 65 | 66 | 67 | ## v1.2.7 - 2022-11-24 68 | 69 | - Security: Update dependencies. 70 | 71 | 72 | ## v1.2.6 - 2022-10-17 73 | 74 | - Security: Update dependencies. 75 | 76 | 77 | ## v1.2.5 - 2022-08-26 78 | 79 | - Security: Update dependencies. 80 | 81 | 82 | ## v1.2.4 - 2022-08-07 83 | 84 | - Fixed: Caching of GitHub Releases did not work probability. 85 | 86 | 87 | ## v1.2.3 - 2022-07-28 88 | 89 | - Security: Update dependencies. 90 | - Fixed: Use correct version in built Neovim. 91 | 92 | 93 | ## v1.2.2 - 2022-06-03 94 | 95 | - Security: Update dependencies. 96 | 97 | 98 | ## v1.2.1 - 2022-02-27 99 | 100 | - Fixed: Fetching the head binary of Neovim. 101 | - Security: Update dependencies. 102 | 103 | 104 | ## v1.2.0 - 2022-02-15 105 | 106 | - Improved: Add `arch` argument MS-Windows. (Thanks [@ichizok](https://github.com/ichizok) [#9](https://github.com/thinca/action-setup-vim/pull/9)) 107 | - Improved: Use the correct path of Visual Studio is MS-Windows. (Thanks [@ichizok](https://github.com/ichizok) [#8](https://github.com/thinca/action-setup-vim/pull/8)) 108 | - Fixed: Wrong using of `LC_TYPE` in MacOS. (Thanks [@ichizok](https://github.com/ichizok) [#7](https://github.com/thinca/action-setup-vim/pull/7)) 109 | 110 | 111 | ## v1.1.2 - 2022-01-17 112 | 113 | - Improved: Reduce consuming API rate limit. (Thanks [@ichizok](https://github.com/ichizok) [#6](https://github.com/thinca/action-setup-vim/pull/6)) 114 | - Fixed: Cannot update the cache of GitHub Releases. (Thanks [@ichizok](https://github.com/ichizok) [#6](https://github.com/thinca/action-setup-vim/pull/6)) 115 | - Security: Update dependencies. 116 | 117 | 118 | ## v1.1.1 - 2022-01-14 119 | 120 | - Fixed: Building Vim on Linux with GUI. 121 | - Fixed: Building MacVim. (Thanks [@ichizok](https://github.com/ichizok) [#5](https://github.com/thinca/action-setup-vim/pull/5)) 122 | 123 | 124 | ## v1.1.0 - 2021-08-14 125 | 126 | - Added: `executable_path` output. 127 | - Improved: Allow installs multiple different Vim at one environment. 128 | - Security: Update dependencies. 129 | 130 | 131 | ## v1.0.9 - 2021-08-01 132 | 133 | - Security: Update dependencies. 134 | 135 | 136 | ## v1.0.8 - 2021-05-23 137 | 138 | - Fixed: Building Neovim v0.4.4. 139 | - Fixed: Building was selected when Neovim's version is `stable` and `download` is `available`. 140 | - Fixed: Building MacVim. 141 | - Security: Update dependencies. 142 | 143 | 144 | ## v1.0.7 - 2020-11-18 145 | 146 | - Added: `install_path` output. 147 | - Added: `cache_hit` output. 148 | - Improved: Improve for old version of Neovim. 149 | - Fixed: Caching problem when multiple jobs run. 150 | - Security: Update dependencies. 151 | 152 | 153 | ## v1.0.6 - 2020-11-03 154 | 155 | - Fixed: Follow to latest MacOS environment of GitHub Actions. 156 | 157 | 158 | ## v1.0.5 - 2020-09-25 159 | 160 | - Security: Update dependencies. 161 | 162 | 163 | ## v1.0.4 - 2020-08-24 164 | 165 | - Fixed: Setup fails sometimes. 166 | - Security: Update dependencies. 167 | 168 | 169 | ## v1.0.3 - 2020-07-29 170 | 171 | - Improved: Cache the GitHub Releases. 172 | - API call is reduced. 173 | - Security: Update dependencies. 174 | 175 | 176 | ## v1.0.2 - 2020-06-07 177 | 178 | - Fixed: Caching feature was always disabled. 179 | 180 | 181 | ## v1.0.1 - 2020-03-25 182 | 183 | - Fixed: Do not select "download" on Linux with Vim v8.1.1238 or older when `download` is `available`. 184 | 185 | 186 | ## v1.0.0 - 2020-03-09 187 | 188 | - First release. 189 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | zlib License 2 | 3 | (C) 2020 thinca 4 | 5 | This software is provided 'as-is', without any express or implied 6 | warranty. In no event will the authors be held liable for any damages 7 | arising from the use of this software. 8 | 9 | Permission is granted to anyone to use this software for any purpose, 10 | including commercial applications, and to alter it and redistribute it 11 | freely, subject to the following restrictions: 12 | 13 | 1. The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would be 16 | appreciated but is not required. 17 | 2. Altered source versions must be plainly marked as such, and must not be 18 | misrepresented as being the original software. 19 | 3. This notice may not be removed or altered from any source distribution. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # setup-vim 2 | 3 | [![Test][test-ci-badge]][test-ci-action] 4 | [![Lint][lint-ci-badge]][lint-ci-action] 5 | 6 | `setup-vim` is a GitHub Action to setup [Vim][vim], [Neovim][neovim], or [MacVim][macvim]. 7 | 8 | 9 | ## Usage 10 | 11 | Basic: 12 | 13 | ```yaml 14 | # Setup the head version of Vim 15 | - uses: thinca/action-setup-vim@v2 16 | ``` 17 | 18 | With options: 19 | 20 | ```yaml 21 | - uses: thinca/action-setup-vim@v2 22 | with: 23 | vim_version: v9.1.0000 24 | ``` 25 | 26 | Setup Vim and Neovim with 2 versions for each platforms using matrix: 27 | 28 | ```yaml 29 | strategy: 30 | matrix: 31 | vim_type: ['Vim', 'Neovim'] 32 | version: ['head', 'stable'] 33 | os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] 34 | include: 35 | - vim_type: 'Vim' 36 | version: 'stable' 37 | vim_version: 'v9.1.0000' 38 | runs-on: '${{ matrix.os }}' 39 | steps: 40 | - uses: 'actions/checkout@v4' 41 | - name: 'Setup Vim' 42 | id: 'vim' 43 | uses: 'thinca/action-setup-vim@v2' 44 | with: 45 | vim_version: '${{ matrix.vim_version || matrix.version }}' 46 | vim_type: '${{ matrix.vim_type }}' 47 | - name: 'Run test' 48 | run: | 49 | # Show Vim's version 50 | ${{ steps.vim.outputs.executable }} --version 51 | # ... run tests ... 52 | ``` 53 | 54 | 55 | ### About installation 56 | 57 | This action provides two ways to setup Vim. 58 | 59 | 1. Build Vim from source code. 60 | You can specify Git's ref(tag, branch, or sha1) for [`vim_version`](#vim_version). 61 | The result is cached by default. See [`cache`](#cache) input. 62 | 63 | 2. Download pre-built Vim from releases page. 64 | You can specify semver(v9.1.0146, v0.9.5) or tag name of GitHub Release for [`vim_version`](#vim_version). 65 | 66 | Some combinations not available. See the following. 67 | By default, uses `download` if available, otherwise uses `build`. 68 | 69 | 70 | #### Vim 71 | 72 | | OS | way | GUI | Installation | 73 | | ------- | ---------- | ------ | ------------------------------------------------------------------ | 74 | | Linux | `build` | `gvim` | Sources from [vim/vim][vim]. | 75 | | Linux | `download` | `gvim` | Releases from [vim/vim-appimage][appimage-releases]. (*) | 76 | | MacOS | `build` | `gvim` | Sources from [vim/vim][vim]. | 77 | | MacOS | `download` | N/A | Not available. | 78 | | Windows | `build` | `gvim` | Sources from [vim/vim][vim]. | 79 | | Windows | `download` | `gvim` | Releases from [vim/vim-win32-installer][win32-installer-releases]. | 80 | 81 | (*) Downloading Vim from AppImage is available from v8.1.1239. Before v8.1.1234 cannot start vim. This was fixed by [vim/vim-appimage#6](https://github.com/vim/vim-appimage/pull/6). 82 | 83 | 84 | #### Neovim 85 | 86 | | OS | way | GUI | Installation | 87 | | ------- | ---------- | ------------- | ----------------------------------------------- | 88 | | Linux | `build` | N/A | Sources from [neovim/neovim][neovim]. | 89 | | Linux | `download` | N/A | Releases from [neovim/neovim][neovim-releases]. | 90 | | MacOS | `build` | N/A | Sources from [neovim/neovim][neovim]. (**) | 91 | | MacOS | `download` | N/A | Releases from [neovim/neovim][neovim-releases]. | 92 | | Windows | `build` | N/A | Not available(Help wanted). | 93 | | Windows | `download` | `nvim-qt.exe` | Releases from [neovim/neovim][neovim-releases]. | 94 | 95 | (**) Building Neovim on MacOS(Catalina) has a problem. 96 | 97 | Building v0.4.3 and before versions will be failure. 98 | See [neovim/neovim#11412](https://github.com/neovim/neovim/pull/11412) for the detail. 99 | 100 | 101 | #### MacVim 102 | 103 | | OS | way | GUI | Installation | 104 | | ------- | ---------- | --- | --------------------------------------------------- | 105 | | Linux | `build` | N/A | Not available. | 106 | | Linux | `download` | N/A | Not available. | 107 | | MacOS | `build` | N/A | Sources from [macvim-dev/macvim][macvim]. | 108 | | MacOS | `download` | N/A | Releases from [macvim-dev/macvim][macvim-releases]. | 109 | | Windows | `build` | N/A | Not available. | 110 | | Windows | `download` | N/A | Not available. | 111 | 112 | MacVim has a GUI version, but it is not supported yet because it is too difficult treating on CI. 113 | 114 | Building snapshot-157 and before versions will be failure. 115 | See [macvim-dev/macvim#946](https://github.com/macvim-dev/macvim/issues/946) for the detail. 116 | 117 | 118 | ### Action Inputs 119 | 120 | #### `vim_version` 121 | 122 | Version of Vim. 123 | The meaning of this value depends on `vim_type` and `download`. 124 | 125 | The value `head` is always head version: 126 | When `download` is on, this points head of release. 127 | When `download` is off, this points master of repository. 128 | 129 | When `download` is on and specified a semver such as `v8.2.0000`, this action finds a minimum version that is higher than a specified version. 130 | For example, when there are some released versions: `v8.2.0052` `v8.2.0057` `v8.2.0065` 131 | And when a specified version is `v8.2.0055`, `v8.2.0057` is actually selected. 132 | Also, when a specified version is `v8.2.0060`, `v8.2.0065` is actually selected. 133 | 134 | When `download` is off, this is a tag of repository. 135 | Note that the repository of MacVim has tags like `release-xxx` instead of like `vx.x.xxx`. 136 | 137 | default: `head` 138 | 139 | 140 | #### `vim_type` 141 | 142 | Type of Vim. 143 | This is one of `vim`, `neovim`, or `macvim`. 144 | 145 | default: `vim` 146 | 147 | 148 | #### `gui` 149 | 150 | When this is `yes`, setups the GUI version. 151 | And `outputs.executable` points to GUI version of Vim. 152 | 153 | default: `no` 154 | 155 | 156 | #### `arch` 157 | 158 | Architecture of Vim. 159 | This is either of `x86_64` or `x86`, enable when `vim_type` is `vim` on Windows. 160 | 161 | default: `x86_64` 162 | 163 | 164 | #### `download` 165 | 166 | When this is `always`, downloads the officially released binary, or fail if unavailable. 167 | When this is `available`, downloads the officially released binary if available, otherwise builds from source code. 168 | When this is `never`, always builds from source code. 169 | 170 | default: `available` 171 | 172 | 173 | #### `cache` 174 | 175 | When this is `true`(default), cache the built Vim. 176 | 177 | This uses same caching mechanism from [actions/cache][actions/cache]. 178 | Therefore, this consumes the limitation of cache size. 179 | 180 | Ref: [Caching dependencies to speed up workflows#Usage limits and eviction policy][caching-policy] 181 | 182 | This is automatically disabled when `download` is on. 183 | 184 | default: `true` 185 | 186 | 187 | #### `github_token` 188 | 189 | Your GitHub API token to access to releases of repositories without limit. 190 | Normally this is automatically set so you do not need set this. 191 | 192 | default: `${{ github.token }}` 193 | 194 | 195 | ### Action Outputs 196 | 197 | #### `executable` 198 | 199 | The name of executable file. 200 | This is not a full path, just name. 201 | When `gui` is yes, this points to GUI version. 202 | e.g. `vim` `nvim` `gvim` 203 | 204 | 205 | #### `executable_path` 206 | 207 | The full path of executable file. 208 | 209 | 210 | #### `actual_vim_version` 211 | 212 | Version of Vim actually installed. 213 | e.g. `v8.2.0123` `v0.4.3` `49cd750d6a72efc0571a89d7a874bbb01081227f` 214 | 215 | 216 | #### `install_type` 217 | 218 | Install was done with `build` or `download`. 219 | 220 | 221 | #### `install_path` 222 | 223 | Base path of installed Vim. 224 | Note that this does not point to `bin`. 225 | 226 | 227 | #### `cache_hit` 228 | 229 | When `cache` is enabled and cache was found, this is `true`. Otherwise this is `false`. 230 | 231 | 232 | ## License 233 | 234 | [zlib License](LICENSE.txt) 235 | 236 | 237 | ## Author 238 | 239 | thinca 240 | 241 | 242 | [test-ci-badge]: ./../../workflows/Test/badge.svg 243 | [test-ci-action]: ./../../actions?query=workflow%3ATest 244 | [lint-ci-badge]: ./../../workflows/Lint/badge.svg 245 | [lint-ci-action]: ./../../actions?query=workflow%3ALint 246 | [vim]: https://github.com/vim/vim 247 | [neovim]: https://github.com/neovim/neovim 248 | [macvim]: https://github.com/macvim-dev/macvim 249 | [appimage-releases]: https://github.com/vim/vim-appimage/releases 250 | [win32-installer-releases]: https://github.com/vim/vim-win32-installer/releases 251 | [neovim-releases]: https://github.com/neovim/neovim/releases 252 | [macvim-releases]: https://github.com/macvim-dev/macvim/releases 253 | [actions/cache]: https://github.com/actions/cache 254 | [caching-policy]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy 255 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup Vim' 2 | description: 'Setup Vim environment and add it to PATH' 3 | author: 'thinca ' 4 | 5 | inputs: 6 | vim_version: 7 | description: |- 8 | Version of Vim. 9 | The meaning of this value depends on `vim_type` and `download`. 10 | 11 | `head` is always head version: 12 | When `download` is enabled, this points head release. 13 | When `download` is not enabled, this points master of repository. 14 | 15 | When `download` is enabled and specified a semver such as `v8.2.0000`, this action finds a minimum version that is higher than a specified version. 16 | For example: 17 | When there are some released versions: `v8.2.0052` `v8.2.0057` `v8.2.0065` 18 | And when a specified version is `v8.2.0055`, `v8.2.0057` is actually selected. 19 | 20 | required: false 21 | default: 'head' 22 | vim_type: 23 | description: |- 24 | Type of Vim. 25 | This is one of `vim`, `neovim`, or `macvim`. 26 | required: false 27 | default: 'vim' 28 | gui: 29 | description: |- 30 | When this is `yes`, setups the GUI version. 31 | And `outputs.executable` points to GUI version of Vim. 32 | required: false 33 | default: 'no' 34 | arch: 35 | description: |- 36 | Architecture of Vim. 37 | This is either of `x86_64` or `x86`, enable when `vim_type` is `vim` on Windows. 38 | required: false 39 | default: 'x86_64' 40 | download: 41 | description: |- 42 | When this is `always`, downloads the officially released binary, or fail if unavailable. 43 | When this is `available`, downloads the officially released binary if available, otherwise builds from source code. 44 | When this is `never`, always builds from source code. 45 | required: false 46 | default: 'available' 47 | cache: 48 | description: |- 49 | When this is `true`(default), cache the built Vim. 50 | 51 | This uses same caching mechanism from actions/cache. 52 | Therefore, this consumes the limitation of cache size. 53 | https://help.github.com/en/actions/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy 54 | 55 | This is automatically disabled when `download` is enabled. 56 | required: false 57 | default: 'true' 58 | github_token: 59 | description: |- 60 | Your GitHub API token to access to releases of repositories without limit. 61 | Normally this is automatically set so you do not need set this. 62 | required: false 63 | default: '${{ github.token }}' 64 | 65 | outputs: 66 | executable: 67 | description: |- 68 | The name of executable file. 69 | This is not a full path, just name. 70 | When `gui` is yes, this points to GUI version. 71 | e.g. `vim` `nvim` `gvim` 72 | executable_path: 73 | description: |- 74 | The full path of executable file. 75 | actual_vim_version: 76 | description: |- 77 | Version of Vim actually installed. 78 | e.g. `v8.2.0123` `v0.4.3` 79 | install_type: 80 | description: |- 81 | Install was done with `build` or `download`. 82 | install_path: 83 | description: |- 84 | Base path of installed Vim. 85 | Note that this does not point to `bin`. 86 | cache_hit: 87 | description: |- 88 | When `cache` is enabled and cache was found, this is `true`. Otherwise this is `false`. 89 | 90 | branding: 91 | icon: 'edit' 92 | color: 'green' 93 | 94 | runs: 95 | using: 'node20' 96 | main: 'action.js' 97 | post: 'action.js' 98 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import globals from "globals"; 3 | import tsParser from "@typescript-eslint/parser"; 4 | import path from "node:path"; 5 | import { fileURLToPath } from "node:url"; 6 | import js from "@eslint/js"; 7 | import { FlatCompat } from "@eslint/eslintrc"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all 15 | }); 16 | 17 | export default [...compat.extends( 18 | "eslint:recommended", 19 | "plugin:@typescript-eslint/eslint-recommended", 20 | "plugin:@typescript-eslint/recommended", 21 | ), { 22 | plugins: { 23 | "@typescript-eslint": typescriptEslint, 24 | }, 25 | 26 | languageOptions: { 27 | globals: { 28 | ...globals.node, 29 | Atomics: "readonly", 30 | SharedArrayBuffer: "readonly", 31 | }, 32 | 33 | parser: tsParser, 34 | ecmaVersion: 2018, 35 | sourceType: "module", 36 | }, 37 | 38 | rules: { 39 | indent: ["error", 2, { 40 | SwitchCase: 1, 41 | }], 42 | 43 | "linebreak-style": ["error", "unix"], 44 | quotes: ["error", "double"], 45 | semi: ["error", "always"], 46 | 47 | "@typescript-eslint/no-unused-vars": ["error", { 48 | args: "all", 49 | argsIgnorePattern: "^_", 50 | caughtErrors: "all", 51 | caughtErrorsIgnorePattern: "^_", 52 | destructuredArrayIgnorePattern: "^_", 53 | varsIgnorePattern: "^_", 54 | }], 55 | }, 56 | }]; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "action-setup-vim", 3 | "description": "A GitHub Action for setting up Vim", 4 | "version": "2.1.2", 5 | "author": "thinca ", 6 | "bugs": { 7 | "url": "https://github.com/thinca/action-setup-vim/issues" 8 | }, 9 | "dependencies": { 10 | "@actions/cache": "^4.0.2", 11 | "@actions/core": "^1.11.1", 12 | "@actions/exec": "^1.1.1", 13 | "@actions/io": "^1.1.3", 14 | "@actions/tool-cache": "^2.0.2", 15 | "@octokit/graphql": "^8.2.1", 16 | "semver": "^7.7.1" 17 | }, 18 | "devDependencies": { 19 | "@eslint/eslintrc": "^3.3.0", 20 | "@eslint/js": "^9.21.0", 21 | "@types/node": "^20.17.19", 22 | "@types/semver": "^7.5.8", 23 | "@typescript-eslint/eslint-plugin": "^8.25.0", 24 | "@typescript-eslint/parser": "^8.25.0", 25 | "esbuild": "0.25.0", 26 | "eslint": "^9.21.0", 27 | "eslint-plugin-import": "^2.31.0", 28 | "globals": "^16.0.0", 29 | "typescript": "^5.7.3" 30 | }, 31 | "homepage": "https://github.com/thinca/action-setup-vim", 32 | "keywords": [ 33 | "ci", 34 | "github", 35 | "github-actions", 36 | "vim" 37 | ], 38 | "license": "Zlib", 39 | "main": "action.js", 40 | "private": true, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/thinca/action-setup-vim.git" 44 | }, 45 | "scripts": { 46 | "build": "esbuild src/index.ts --bundle --platform=node --outfile=action.js", 47 | "lint": "eslint '**/*.ts'", 48 | "test": "echo \"Error: no test specified\" && exit 1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Release script 4 | # 5 | # Usage: ./script/release {major|minor|patch} 6 | # Run on develop branch. 7 | # 8 | # This script executes: 9 | # 1. Merge "develop" into "master" 10 | # 2. Version bump in CHANGELOG.md, README.md, package{,-lock}.json, index.ts and commit 11 | # 3. Create a new tag with annotation containing release note 12 | 13 | set -eu 14 | 15 | version=$1 16 | 17 | if [[ -z ${version} ]]; then 18 | echo "Usage: $0 {version}" >&2 19 | exit 64 20 | fi 21 | 22 | current_branch=$(git branch --show-current) 23 | 24 | if [[ ${current_branch} != 'develop' ]]; then 25 | echo 'You must release on "develop" branch.' >&2 26 | exit 10 27 | fi 28 | 29 | if [[ ! -f CHANGELOG.md ]]; then 30 | echo 'CHANGELOG.md not found.' >&2 31 | exit 10 32 | fi 33 | 34 | release_note=$(sed -e '1,/## Unreleased/d' -e '/^##/,$d' CHANGELOG.md | sed -z -e 's/^\s\+//') 35 | if [[ -z "${release_note}" ]]; then 36 | echo 'Release note not found.' >&2 37 | exit 10 38 | fi 39 | 40 | git checkout master 41 | git merge --no-edit --no-ff develop 42 | 43 | new_version=$(npm version --no-git-tag-version "${version}") 44 | new_version_major=$(sed -e 's/\..*//' <<< "${new_version}") 45 | 46 | new_version_header="${new_version} - $(date '+%Y-%m-%d')" 47 | tag_message="${new_version_header} 48 | 49 | ${release_note}" 50 | 51 | sed -i -e "s;thinca/action-setup-vim@v[0-9]\\+;thinca/action-setup-vim@${new_version_major};" README.md 52 | sed -i -e "s/^## Unreleased$/## ${new_version_header}/" CHANGELOG.md 53 | sed -i -e "s/^const actionVersion = \".\\+\";$/const actionVersion = \"${new_version#v}\";/" src/index.ts 54 | git add README.md CHANGELOG.md src/index.ts package.json package-lock.json 55 | 56 | git commit -m "${new_version}" 57 | echo "${tag_message}" | git tag -s -F - "${new_version}+src" 58 | 59 | git checkout develop 60 | git merge --no-edit --no-ff master 61 | 62 | sed -i '3i ## Unreleased\ 63 | \ 64 | \ 65 | ' CHANGELOG.md 66 | git add CHANGELOG.md 67 | git commit -m "Add a new CHANGELOG entry for next version" 68 | -------------------------------------------------------------------------------- /src/action_error.ts: -------------------------------------------------------------------------------- 1 | export class ActionError extends Error {} 2 | -------------------------------------------------------------------------------- /src/commands.ts: -------------------------------------------------------------------------------- 1 | import * as cp from "child_process"; 2 | import * as fs from "fs"; 3 | import {exec} from "@actions/exec"; 4 | 5 | export function execGit(args: Array, options = {}): Promise { 6 | return new Promise((resolve, reject) => { 7 | const defaultOptions = {env: Object.assign({}, process.env, {GIT_TERMINAL_PROMPT: "0"})}; 8 | cp.execFile("git", args, Object.assign(defaultOptions, options), (error, resultText, errorText) => { 9 | if (error) { 10 | reject(new Error(JSON.stringify({exitCode: error.code, resultText, errorText}))); 11 | } else { 12 | resolve(resultText); 13 | } 14 | }); 15 | }); 16 | } 17 | 18 | export async function gitClone(ghRepo: string, vimVersion: string, dir: string, depth: number | null = 1): Promise { 19 | if (fs.existsSync(dir)) { 20 | return; 21 | } 22 | const args = ["clone"]; 23 | args.unshift("-c", "advice.detachedHead=false"); 24 | args.push("--quiet"); 25 | if (depth != null) { 26 | args.push("--depth", `${depth}`); 27 | } 28 | args.push("--branch", vimVersion, `https://github.com/${ghRepo}`, dir); 29 | await exec("git", args); 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/get_installer.ts: -------------------------------------------------------------------------------- 1 | import {ActionError} from "./action_error"; 2 | import {Installer, VimType} from "./interfaces"; 3 | 4 | import {LinuxNeovimBuildInstaller} from "./installer/linux_neovim_build_installer"; 5 | import {LinuxNeovimReleasesInstaller} from "./installer/linux_neovim_releases_installer"; 6 | import {LinuxVimReleasesInstaller} from "./installer/linux_vim_releases_installer"; 7 | import {MacVimBuildInstaller} from "./installer/macvim_build_installer"; 8 | import {MacVimReleasesInstaller} from "./installer/macvim_releases_installer"; 9 | import {MacosNeovimBuildInstaller} from "./installer/macos_neovim_build_installer"; 10 | import {MacosNeovimReleasesInstaller} from "./installer/macos_neovim_releases_installer"; 11 | import {UnixVimBuildInstaller} from "./installer/unix_vim_build_installer"; 12 | import {WindowsNeovimReleasesInstaller} from "./installer/windows_neovim_releases_installer"; 13 | import {WindowsVimBuildInstaller} from "./installer/windows_vim_build_installer"; 14 | import {WindowsVimReleasesInstaller} from "./installer/windows_vim_releases_installer"; 15 | 16 | class InstallerUnavailableError extends ActionError {} 17 | 18 | function _getInstaller(installDir: string, vimType: VimType, isGUI: boolean, isDownload: boolean): Installer { 19 | switch (process.platform) { 20 | case "linux": 21 | switch (vimType) { 22 | case VimType.vim: 23 | if (isDownload) { 24 | return new LinuxVimReleasesInstaller(installDir, isGUI); 25 | } else { 26 | return new UnixVimBuildInstaller(installDir, isGUI); 27 | } 28 | case VimType.neovim: 29 | if (isDownload) { 30 | return new LinuxNeovimReleasesInstaller(installDir, isGUI); 31 | } else { 32 | return new LinuxNeovimBuildInstaller(installDir, isGUI); 33 | } 34 | } 35 | throw new ActionError(`Unsupported vim_type in Linux: ${vimType}`); 36 | case "darwin": 37 | if (isGUI) { 38 | throw new ActionError("GUI is not supported in MacOS"); 39 | } 40 | switch (vimType) { 41 | case VimType.vim: 42 | if (isDownload) { 43 | throw new InstallerUnavailableError("Download is not supported with MacOS/Vim"); 44 | } else { 45 | return new UnixVimBuildInstaller(installDir, isGUI); 46 | } 47 | case VimType.neovim: 48 | if (isDownload) { 49 | return new MacosNeovimReleasesInstaller(installDir, isGUI); 50 | } else { 51 | return new MacosNeovimBuildInstaller(installDir, isGUI); 52 | } 53 | case VimType.macvim: 54 | if (isDownload) { 55 | return new MacVimReleasesInstaller(installDir, isGUI); 56 | } else { 57 | return new MacVimBuildInstaller(installDir, isGUI); 58 | } 59 | } 60 | // here is unreachable so cannot put "break;" 61 | // eslint-disable-next-line no-fallthrough 62 | case "win32": 63 | switch (vimType) { 64 | case VimType.vim: 65 | if (isDownload) { 66 | return new WindowsVimReleasesInstaller(installDir, isGUI); 67 | } else { 68 | return new WindowsVimBuildInstaller(installDir, isGUI); 69 | } 70 | case VimType.neovim: 71 | if (isDownload) { 72 | return new WindowsNeovimReleasesInstaller(installDir, isGUI); 73 | } else { 74 | // TODO: Build Neovim on Windows 75 | } 76 | } 77 | throw new ActionError(`Unsupported vim_type in Windows: ${vimType}`); 78 | } 79 | throw new ActionError(`Unsupported platform: ${process.platform}`); 80 | } 81 | 82 | export function getInstaller(installDir: string, vimType: VimType, isGUI: boolean, download: string, version: string): Installer { 83 | switch (download) { 84 | case "always": 85 | return _getInstaller(installDir, vimType, isGUI, true); 86 | case "available": 87 | try { 88 | const installer = _getInstaller(installDir, vimType, isGUI, true); 89 | if (!installer.canInstall(version)) { 90 | throw new InstallerUnavailableError(); 91 | } 92 | return installer; 93 | } catch (e) { 94 | if (e instanceof InstallerUnavailableError) { 95 | return _getInstaller(installDir, vimType, isGUI, false); 96 | } 97 | throw e; 98 | } 99 | case "never": 100 | return _getInstaller(installDir, vimType, isGUI, false); 101 | } 102 | throw new ActionError(`Invalid download parameter: ${download}`); 103 | } 104 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import * as core from "@actions/core"; 4 | import * as io from "@actions/io"; 5 | import {ActionError} from "./action_error"; 6 | import {VimType, isVimType} from "./interfaces"; 7 | import * as cache from "@actions/cache"; 8 | import {getInstaller} from "./get_installer"; 9 | import {TEMP_PATH} from "./temp"; 10 | 11 | const actionVersion = "2.1.2"; 12 | 13 | function makeCacheKey(vimType: VimType, isGUI: boolean, vimVersion: string, download: string): string { 14 | return `${actionVersion}-${process.platform}-${vimType}-${isGUI ? "gui" : "cui"}-${download}-${vimVersion}`; 15 | } 16 | 17 | 18 | async function main(): Promise { 19 | const vimType = core.getInput("vim_type").toLowerCase(); 20 | if (!isVimType(vimType)) { 21 | throw new ActionError(`Invalid vim_type: ${vimType}`); 22 | } 23 | 24 | const download = core.getInput("download"); 25 | const isGUI = core.getInput("gui") === "yes"; 26 | const inputVimVersion = core.getInput("vim_version"); 27 | if (!/^[a-zA-Z0-9.+-]+$/.test(inputVimVersion)) { 28 | throw new ActionError(`Invalid vim_version: ${inputVimVersion}`); 29 | } 30 | 31 | const installPath = path.join(TEMP_PATH, vimType, inputVimVersion); 32 | const installer = getInstaller(installPath, vimType, isGUI, download, inputVimVersion); 33 | 34 | const fixedVersion = await installer.resolveVersion(inputVimVersion); 35 | core.info(`Vim version: ${fixedVersion}`); 36 | 37 | let installed = false; 38 | try { 39 | const stat = fs.statSync(installPath); 40 | installed = stat.isDirectory(); 41 | } catch (_e) { 42 | // path not exist 43 | } 44 | 45 | let cacheHit: string | undefined; 46 | 47 | if (!installed) { 48 | await io.mkdirP(installPath); 49 | 50 | const cacheInput = core.getInput("cache"); 51 | const cacheTest = /^test-/.test(cacheInput); 52 | const useCache = installer.installType == "build" && (cacheInput === "true" || cacheTest); 53 | 54 | if (useCache) { 55 | const cacheKeyPrefixForTest = cacheTest ? `${cacheInput}-` : ""; 56 | const cacheKey = `${cacheKeyPrefixForTest}${makeCacheKey(vimType, isGUI, fixedVersion, download)}`; 57 | cacheHit = await cache.restoreCache([installPath], cacheKey, []); 58 | if (!cacheHit) { 59 | await installer.install(fixedVersion); 60 | 61 | // For test: Write cache immediately for next step. 62 | if (cacheTest) { 63 | await saveCache(installPath, cacheKey); 64 | } else { 65 | core.saveState("cache_key", cacheKey); 66 | core.saveState("install_path", installPath); 67 | } 68 | } 69 | } else { 70 | core.info("Cache disabled"); 71 | await installer.install(fixedVersion); 72 | } 73 | } 74 | 75 | const binPath = installer.getPath(fixedVersion); 76 | const executableName = installer.getExecutableName(); 77 | const executableFullName = executableName + (process.platform === "win32" ? ".exe" : ""); 78 | 79 | core.addPath(binPath); 80 | core.setOutput("actual_vim_version", fixedVersion); 81 | core.setOutput("executable", executableName); 82 | core.setOutput("executable_path", path.join(binPath, executableFullName)); 83 | core.setOutput("install_type", installer.installType); 84 | core.setOutput("install_path", installPath); 85 | core.setOutput("cache_hit", cacheHit ? "true" : "false"); 86 | } 87 | 88 | async function saveCache(installPath: string, cacheKey: string): Promise { 89 | try { 90 | await cache.saveCache([installPath], cacheKey); 91 | } catch (e) { 92 | if (e instanceof cache.ReserveCacheError) { 93 | core.debug(`Error while caching binary in post: ${e.name}: ${e.message}`); 94 | } else { 95 | throw e; 96 | } 97 | } 98 | } 99 | 100 | async function post(): Promise { 101 | const cacheKey = core.getState("cache_key"); 102 | if (cacheKey) { 103 | const installPath = core.getState("install_path"); 104 | await saveCache(installPath, cacheKey); 105 | } 106 | } 107 | 108 | async function run(): Promise { 109 | const isPost = !!core.getState("isPost"); 110 | if (isPost) { 111 | await post(); 112 | } else { 113 | core.saveState("isPost", "true"); 114 | await main(); 115 | } 116 | } 117 | 118 | 119 | run().catch(e => { 120 | if (!(e instanceof ActionError) && e.stack) { 121 | core.error(e.stack); 122 | } 123 | const message = e.message || JSON.stringify(e); 124 | core.setFailed(message); 125 | }); 126 | -------------------------------------------------------------------------------- /src/installer/build_installer.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import {execGit, gitClone} from "../commands"; 3 | import {FixedVersion, Installer, InstallType} from "../interfaces"; 4 | import {TEMP_PATH} from "../temp"; 5 | 6 | function isFixedVersion(vimVersion: string): boolean { 7 | return /^v\d/.test(vimVersion); 8 | } 9 | 10 | export abstract class BuildInstaller implements Installer { 11 | abstract readonly repository: string; 12 | abstract getExecutableName(): string; 13 | abstract install(vimVersion: FixedVersion): Promise; 14 | abstract getPath(vimVersion: FixedVersion): string; 15 | 16 | readonly installType = InstallType.build; 17 | readonly installDir: string; 18 | readonly isGUI: boolean; 19 | 20 | private _repositoryPath?: string; 21 | 22 | constructor(installDir: string, isGUI: boolean) { 23 | this.installDir = installDir; 24 | this.isGUI = isGUI; 25 | } 26 | 27 | // Currently, BuildInstaller.canInstall() is not used. 28 | canInstall(): boolean { 29 | return true; 30 | } 31 | 32 | repositoryPath(vimVersion: string): string { 33 | if (!this._repositoryPath) { 34 | const repoName = this.repository.split("/").pop() || "vim"; 35 | this._repositoryPath = path.join(TEMP_PATH, "repos", repoName, vimVersion); 36 | } 37 | return this._repositoryPath; 38 | } 39 | 40 | async obtainFixedVersion(vimVersion: string): Promise { 41 | return await execGit(["describe", "--tags", "--always"], {cwd: this.repositoryPath(vimVersion)}); 42 | } 43 | 44 | async resolveVersion(vimVersion: string): Promise { 45 | if (vimVersion === "head") { 46 | vimVersion = "master"; 47 | } 48 | 49 | if (!isFixedVersion(vimVersion)) { 50 | await gitClone(this.repository, vimVersion, this.repositoryPath(vimVersion), null); 51 | vimVersion = await this.obtainFixedVersion(vimVersion); 52 | if (!isFixedVersion(vimVersion)) { 53 | vimVersion = await execGit(["rev-parse", "HEAD"], {cwd: this.repositoryPath(vimVersion)}); 54 | } 55 | } 56 | 57 | return vimVersion.trim() as FixedVersion; 58 | } 59 | 60 | async cloneVim(vimVersion: string): Promise { 61 | const reposPath = this.repositoryPath(vimVersion); 62 | await gitClone(this.repository, vimVersion, reposPath, 1); 63 | return reposPath; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/installer/linux_neovim_build_installer.ts: -------------------------------------------------------------------------------- 1 | import {exec} from "@actions/exec"; 2 | import {FixedVersion} from "../interfaces"; 3 | import {NeovimBuildInstaller} from "./neovim_build_installer"; 4 | import * as path from "path"; 5 | import {readFileSync} from "fs"; 6 | 7 | export class LinuxNeovimBuildInstaller extends NeovimBuildInstaller { 8 | async install(vimVersion: FixedVersion): Promise { 9 | const reposPath = await this.cloneVim(vimVersion); 10 | 11 | const packages = [ 12 | "ninja-build", "gettext", "libtool", "libtool-bin", 13 | "autoconf", "automake", "cmake", "g++", "pkg-config", "unzip", 14 | ]; 15 | const configureArgs = [ 16 | "CMAKE_BUILD_TYPE=RelWithDebInfo", 17 | `CMAKE_EXTRA_FLAGS=-DCMAKE_INSTALL_PREFIX=${this.installDir}`, 18 | ]; 19 | 20 | // XXX: Trick for v0.2.x. This condition is not strict. 21 | if (this.findLine(path.join(reposPath, "third-party/cmake/BuildLuarocks.cmake"), "luacheck-scm-1.rockspec")) { 22 | packages.push("libuv1-dev", "libmsgpack-dev", "libtermkey-dev", "lua5.2", "lua-lpeg", "lua-mpack", "lua-bitop", "libluajit-5.1-dev", "gperf"); 23 | configureArgs.push("DEPS_CMAKE_FLAGS=-DUSE_BUNDLED=OFF -DUSE_BUNDLED_LIBVTERM=ON -DUSE_BUNDLED_UNIBILIUM=ON"); 24 | } 25 | 26 | // Workaround: 27 | // GitHub-hosted runner has CMake 3.20. 28 | // But, cannot build Neovim v0.4.4 with CMake 3.20. 29 | // So delete it and use CMake 3.16 from apt-get. 30 | await exec("sudo", ["rm", "-f", "/usr/local/bin/cmake"]); 31 | 32 | await exec("sudo", ["apt-get", "update"]); 33 | await exec("sudo", ["apt-get", "install", ...packages]); 34 | await exec("make", configureArgs, {cwd: reposPath}); 35 | await exec("make", ["install"], {cwd: reposPath}); 36 | } 37 | 38 | findLine(filepath: string, target: string): boolean { 39 | try { 40 | const content = readFileSync(filepath, {encoding: "utf8"}); 41 | return 0 <= content.indexOf(target); 42 | } catch (_e) { 43 | return false; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/installer/linux_neovim_releases_installer.ts: -------------------------------------------------------------------------------- 1 | import {toSemver} from "./releases_installer"; 2 | import {NeovimReleasesInstaller} from "./neovim_releases_installer"; 3 | 4 | export class LinuxNeovimReleasesInstaller extends NeovimReleasesInstaller { 5 | readonly assetNamePatterns: RegExp[] = [ 6 | RegExp(String.raw`^nvim-linux(?:-${this.arch}|64)\.tar\.gz$`), 7 | /^nvim\.appimage$/, 8 | ]; 9 | 10 | canInstall(version: string): boolean { 11 | if (version === "stable" || version === "nightly" || version === "head") { 12 | return true; 13 | } 14 | const semver = toSemver(version); 15 | return !!semver && 0 <= semver.compare("0.3.0"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/installer/linux_vim_releases_installer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import * as semver from "semver"; 4 | import * as core from "@actions/core"; 5 | import {exec} from "@actions/exec"; 6 | import * as io from "@actions/io"; 7 | import {SemverReleasesInstaller} from "./semver_releases_installer"; 8 | import {FixedVersion} from "../interfaces"; 9 | 10 | export class LinuxVimReleasesInstaller extends SemverReleasesInstaller { 11 | readonly repository: string = "vim/vim-appimage"; 12 | readonly assetNamePatterns: RegExp[] = [/\.AppImage$/]; 13 | readonly availableVersion: string = "v8.1.1239"; 14 | 15 | getExecutableName(): string { 16 | return this.isGUI ? "gvim" : "vim"; 17 | } 18 | 19 | async install(vimVersion: FixedVersion): Promise { 20 | const ubuntuVersion = getUbuntuVersion(); 21 | // libfuse2 is needed for appimage. 22 | const packages = ["libfuse2"]; 23 | if (ubuntuVersion === "22.04") { 24 | // LD_PRELOAD are needed for appimage on Ubuntu 22.04. 25 | core.exportVariable("LD_PRELOAD", "/lib/x86_64-linux-gnu/libgmodule-2.0.so"); 26 | } 27 | if (ubuntuVersion === "24.04") { 28 | const vimSemver = semver.coerce(vimVersion, {loose: true}); 29 | if (vimSemver && semver.lt(vimSemver, "8.2.5114", true)) { 30 | packages.push("libglib2.0-dev"); 31 | core.exportVariable("LD_PRELOAD", "/lib/x86_64-linux-gnu/libgmodule-2.0.so"); 32 | } 33 | } 34 | await exec("sudo", ["apt-get", "update"]); 35 | await exec("sudo", ["apt-get", "install", ...packages]); 36 | 37 | const archiveFilePath = await this.downloadAsset(vimVersion); 38 | const installPath = this.getPath(); 39 | await io.mkdirP(installPath); 40 | const binVim = path.join(installPath, this.getExecutableName()); 41 | await io.mv(archiveFilePath, binVim); 42 | fs.chmodSync(binVim, 0o777); 43 | } 44 | 45 | getPath(): string { 46 | return path.join(this.installDir, "bin"); 47 | } 48 | } 49 | 50 | function getUbuntuVersion(): string { 51 | const version = fs.readFileSync("/etc/os-release", "utf8"); 52 | const matched = /VERSION_ID="(\d+\.\d+)"/.exec(version); 53 | return matched ? matched[1] : ""; 54 | } 55 | -------------------------------------------------------------------------------- /src/installer/macos_neovim_build_installer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import {exec} from "@actions/exec"; 4 | import {FixedVersion} from "../interfaces"; 5 | import {NeovimBuildInstaller} from "./neovim_build_installer"; 6 | 7 | export class MacosNeovimBuildInstaller extends NeovimBuildInstaller { 8 | async install(vimVersion: FixedVersion): Promise { 9 | const reposPath = await this.cloneVim(vimVersion); 10 | const packages = [ 11 | "ninja", "libtool", "automake", 12 | ]; 13 | await exec("brew", ["install", ...packages]); 14 | 15 | const makeLists = fs.readFileSync(path.join(reposPath, "src", "nvim", "CMakeLists.txt"), "utf-8"); 16 | if (!/-Wl,-no_deduplicate/.test(makeLists)) { 17 | let newMakeLists: string; 18 | if (/\$\{CMAKE_EXE_LINKER_FLAGS} -framework CoreServices/.test(makeLists)) { 19 | newMakeLists = makeLists.replace(/\$\{CMAKE_EXE_LINKER_FLAGS} -framework CoreServices/, "$& -Wl,-no_deduplicate"); 20 | } else { 21 | newMakeLists = makeLists.replace(/target_link_libraries\((\w+) PRIVATE "-framework CoreServices"\)/, "$&\n target_link_options($1 PRIVATE \"-Wl,-no_deduplicate\")"); 22 | } 23 | if (newMakeLists !== makeLists) { 24 | fs.writeFileSync(path.join(reposPath, "src", "nvim", "CMakeLists.txt"), newMakeLists); 25 | } 26 | } 27 | 28 | // Build fails with Xcode 11.1 (default) 29 | await exec("make", ["CMAKE_BUILD_TYPE=RelWithDebInfo", "MACOSX_DEPLOYMENT_TARGET=10.14", `CMAKE_EXTRA_FLAGS=-DCMAKE_INSTALL_PREFIX=${this.installDir}`], {cwd: reposPath}); 30 | await exec("make", ["install"], {cwd: reposPath}); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/installer/macos_neovim_releases_installer.ts: -------------------------------------------------------------------------------- 1 | import {NeovimReleasesInstaller} from "./neovim_releases_installer"; 2 | 3 | export class MacosNeovimReleasesInstaller extends NeovimReleasesInstaller { 4 | readonly assetNamePatterns: RegExp[] = [RegExp(String.raw`^nvim-macos(?:-${this.arch})?\.tar\.gz$`)]; 5 | } 6 | -------------------------------------------------------------------------------- /src/installer/macvim_build_installer.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import {exec} from "@actions/exec"; 3 | import * as io from "@actions/io"; 4 | import {BuildInstaller} from "./build_installer"; 5 | import {execGit} from "../commands"; 6 | import {FixedVersion} from "../interfaces"; 7 | import {backportPatch} from "../patch"; 8 | import {Buffer} from "buffer"; 9 | import * as semver from "semver"; 10 | 11 | export class MacVimBuildInstaller extends BuildInstaller { 12 | readonly repository = "macvim-dev/macvim"; 13 | readonly tags: { [key: string]: string } = {}; 14 | 15 | getExecutableName(): string { 16 | return "vim"; 17 | } 18 | 19 | async obtainFixedVersion(vimVersion: string): Promise { 20 | const log = await execGit(["log", "-20", "--format=format:%s"], {cwd: this.repositoryPath(vimVersion)}); 21 | const matched = /^\s*(?:patch|updated for version)\s+v?(\d+\.\d+\.\d+)/m.exec(log); 22 | if (matched) { 23 | const version = `v${matched[1]}`; 24 | const tag = await super.obtainFixedVersion(vimVersion); 25 | this.tags[version] = tag; 26 | return version; 27 | } 28 | return ""; 29 | } 30 | 31 | async install(vimVersion: FixedVersion): Promise { 32 | await exec("xcode-select", ["-p"]); 33 | const tag = this.tags[vimVersion] || vimVersion; 34 | const reposPath = await this.cloneVim(tag); 35 | await backportPatch(reposPath, vimVersion, this.isGUI); 36 | 37 | // To avoid `sed: RE error: illegal byte sequence` error, should set 'LC_ALL=C'. 38 | process.env.LC_ALL = "C"; 39 | 40 | const args: string[] = []; 41 | if (semver.lte(vimVersion, "8.2.2127", true)) { 42 | // Build only x86_64 arch since Sparkle.framework < 1.24.0 doesn't support Apple Silicon. 43 | if (semver.lte(vimVersion, "8.2.2013", true)) { 44 | args.push("--with-macarchs=x86_64"); 45 | } 46 | 47 | // Fix broken "--with-macarchs" flag. 48 | const patch = ` 49 | --- a/src/auto/configure 50 | +++ b/src/auto/configure 51 | @@ -4758,7 +4758,7 @@ fi 52 | $as_echo_n "checking if architectures are supported... " >&6; } 53 | save_cflags="$CFLAGS" 54 | save_ldflags="$LDFLAGS" 55 | - archflags=\`echo "$ARCHS" | sed -e 's/[[:<:]]/-arch /g'\` 56 | + archflags=\`echo "$ARCHS" | sed 's/[[:>:]][ ][ ]*[[:<:]]/ -arch /g' | sed 's/^/-arch /g'\` 57 | CFLAGS="$CFLAGS $archflags" 58 | LDFLAGS="$LDFLAGS $archflags" 59 | cat confdefs.h - <<_ACEOF >conftest.$ac_ext 60 | `.trim() + "\n"; 61 | await exec("patch", ["-p1"], {cwd: reposPath, input: Buffer.from(patch)}); 62 | } 63 | 64 | if (semver.lte(vimVersion, "8.2.5135", true)) { 65 | args.push("CFLAGS=-Wno-implicit-int"); 66 | } 67 | 68 | await exec("./configure", args, {cwd: reposPath}); 69 | await exec("make", [], {cwd: reposPath}); 70 | await io.mkdirP(this.installDir); 71 | await io.cp(path.join(reposPath, "src", "MacVim", "build", "Release", "MacVim.app"), this.installDir, {recursive: true}); 72 | } 73 | 74 | getPath(): string { 75 | return path.join(this.installDir, "MacVim.app", "Contents", "bin"); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/installer/macvim_releases_installer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import {exec} from "@actions/exec"; 4 | import * as io from "@actions/io"; 5 | import {ReleasesInstaller, Release} from "./releases_installer"; 6 | import {FixedVersion} from "../interfaces"; 7 | 8 | function checkPath(targetPath: string): boolean { 9 | try { 10 | const stat = fs.statSync(targetPath); 11 | return stat.isFile() || stat.isDirectory(); 12 | } catch (_e) { 13 | return false; 14 | } 15 | } 16 | 17 | export class MacVimReleasesInstaller extends ReleasesInstaller { 18 | readonly repository: string = "macvim-dev/macvim"; 19 | readonly assetNamePatterns: RegExp[] = [/^MacVim.*\.dmg$/]; 20 | readonly vimVersionPattern: RegExp = /(?:Vim\s+patch|Updated\s+to\s+Vim)\s*(\d+\.\d+\.\d+)/i; 21 | 22 | toSemverString(release: Release): string { 23 | const matched = this.vimVersionPattern.exec(release.description ?? ""); 24 | return matched?.[1] || ""; 25 | } 26 | 27 | async install(vimVersion: FixedVersion): Promise { 28 | const archiveFilePath = await this.downloadAsset(vimVersion); 29 | await exec("hdiutil", ["attach", "-quiet", "-mountpoint", "/Volumes/MacVim", archiveFilePath]); 30 | await io.mkdirP(this.installDir); 31 | await io.cp("/Volumes/MacVim/MacVim.app", this.installDir, {recursive: true}); 32 | await exec("hdiutil", ["detach", "/Volumes/MacVim"]); 33 | } 34 | 35 | getPath(): string { 36 | const vimPath = path.join(this.installDir, "MacVim.app", "Contents", "bin"); 37 | if (checkPath(path.join(vimPath, "vim"))) { 38 | return vimPath; 39 | } 40 | const oldVimPath = path.join(this.installDir, "MacVim.app", "Contents", "MacOS"); 41 | if (checkPath(path.join(oldVimPath, "Vim"))) { 42 | return oldVimPath; 43 | } 44 | throw new Error("Vim executable could not found"); 45 | } 46 | 47 | getExecutableName(): string { 48 | const vimPath = path.join(this.installDir, "MacVim.app", "Contents", "bin", "vim"); 49 | if (checkPath(vimPath)) { 50 | return "vim"; 51 | } 52 | const oldVimPath = path.join(this.installDir, "MacVim.app", "Contents", "MacOS", "Vim"); 53 | if (checkPath(oldVimPath)) { 54 | return "Vim"; 55 | } 56 | throw new Error("Vim executable could not found"); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/installer/neovim_build_installer.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import {BuildInstaller} from "./build_installer"; 3 | import {FixedVersion} from "../interfaces"; 4 | 5 | export abstract class NeovimBuildInstaller extends BuildInstaller { 6 | readonly repository = "neovim/neovim"; 7 | 8 | getExecutableName(): string { 9 | return "nvim"; 10 | } 11 | 12 | getPath(): string { 13 | return path.join(this.installDir, "bin"); 14 | } 15 | 16 | abstract install(vimVersion: FixedVersion): Promise; 17 | } 18 | -------------------------------------------------------------------------------- /src/installer/neovim_releases_installer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import {extractZip, extractTar} from "@actions/tool-cache"; 4 | import {exec} from "@actions/exec"; 5 | import {SemverReleasesInstaller} from "./semver_releases_installer"; 6 | import {FixedVersion} from "../interfaces"; 7 | 8 | export abstract class NeovimReleasesInstaller extends SemverReleasesInstaller { 9 | readonly repository: string = "neovim/neovim"; 10 | readonly arch = process.arch === "arm64" ? "arm64" : "x86_64"; 11 | 12 | async install(vimVersion: FixedVersion): Promise { 13 | const archiveFilePath = await this.downloadAsset(vimVersion); 14 | const ext = path.extname(archiveFilePath).toLowerCase(); 15 | if (ext === ".appimage") { 16 | fs.mkdirSync(path.join(this.installDir, "bin")); 17 | const executablePath = path.join(this.installDir, "bin", "nvim"); 18 | fs.renameSync(archiveFilePath, executablePath); 19 | fs.chmodSync(executablePath, 0o755); 20 | } else { 21 | const installDir = 22 | ext === ".zip" ? 23 | await extractZip(archiveFilePath, this.installDir) : 24 | await extractTar(archiveFilePath, this.installDir); 25 | const dirs = fs.readdirSync(installDir); 26 | if (dirs.length !== 1) { 27 | throw new Error(`Unexpected archive entries: ${JSON.stringify(dirs)}`); 28 | } 29 | await exec("bash", ["-c", `mv '${dirs[0]}'/* .`], {cwd: installDir}); 30 | fs.rmdirSync(path.join(installDir, dirs[0])); 31 | } 32 | } 33 | 34 | getPath(): string { 35 | return path.join(this.installDir, "bin"); 36 | } 37 | 38 | getExecutableName(): string { 39 | return "nvim"; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/installer/releases_installer.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import * as semver from "semver"; 5 | import * as cache from "@actions/cache"; 6 | import {downloadTool} from "@actions/tool-cache"; 7 | import {graphql} from "@octokit/graphql"; 8 | import {RequestParameters} from "@octokit/graphql/dist-types/types"; 9 | import {ActionError} from "../action_error"; 10 | import {FixedVersion, Installer, InstallType} from "../interfaces"; 11 | import {TEMP_PATH} from "../temp"; 12 | 13 | const RELEASES_QUERY = ` 14 | query($owner: String!, $repo: String!, $cursor: String) { 15 | repository(owner: $owner, name: $repo) { 16 | releases(first: 100, orderBy: {direction: DESC, field: CREATED_AT}, after: $cursor) { 17 | edges { 18 | node { 19 | description 20 | isLatest 21 | releaseAssets(first: 20) { 22 | edges { 23 | node { 24 | name 25 | downloadUrl 26 | } 27 | } 28 | } 29 | tagCommit { 30 | oid 31 | } 32 | tagName 33 | updatedAt 34 | } 35 | } 36 | pageInfo { 37 | endCursor 38 | hasNextPage 39 | } 40 | } 41 | } 42 | }`; 43 | 44 | export type Release = { 45 | description: string; 46 | isLatest: boolean; 47 | releaseAssets: { 48 | edges: { 49 | node: { 50 | name: string; 51 | downloadUrl: string; 52 | }; 53 | }[]; 54 | }; 55 | tagCommit: { 56 | oid: string; 57 | }; 58 | tagName: string; 59 | updatedAt: string; 60 | } 61 | 62 | type Response = { 63 | repository: { 64 | releases: { 65 | pageInfo: { 66 | hasNextPage: boolean; 67 | endCursor: string; 68 | } 69 | edges: { 70 | node: Release; 71 | }[]; 72 | }; 73 | }; 74 | } 75 | 76 | type DeepPartial = { 77 | [P in keyof T]?: T[P] extends Array ? Array> : DeepPartial; 78 | } 79 | 80 | export function toSemver(ver: string): semver.SemVer | null { 81 | if (/^v?\d/.test(ver)) { 82 | return semver.coerce(ver, {loose: true}); 83 | } 84 | return null; 85 | } 86 | 87 | export abstract class ReleasesInstaller implements Installer { 88 | abstract readonly repository: string; 89 | abstract readonly assetNamePatterns: RegExp[]; 90 | abstract getExecutableName(): string; 91 | abstract toSemverString(release: Release): string; 92 | abstract install(vimVersion: FixedVersion): Promise; 93 | abstract getPath(vimVersion: FixedVersion): string; 94 | 95 | readonly availableVersion: string = ""; 96 | readonly installType = InstallType.download; 97 | readonly installDir: string; 98 | readonly isGUI: boolean; 99 | 100 | private release?: Release; 101 | private releases: Release[] = []; 102 | 103 | constructor(installDir: string, isGUI: boolean) { 104 | this.installDir = installDir; 105 | this.isGUI = isGUI; 106 | } 107 | 108 | canInstall(_version: string): boolean { 109 | return true; 110 | } 111 | 112 | async resolveVersion(vimVersion: string): Promise { 113 | this.releases = await this.fetchReleases(); 114 | this.release = this.findRelease(vimVersion); 115 | const actualVersion = this.perpetuateVersion(); 116 | return actualVersion as FixedVersion; 117 | } 118 | 119 | private async fetchReleases(): Promise { 120 | const [owner, repo] = this.repository.split("/"); 121 | const parameters: RequestParameters = { 122 | owner: owner, 123 | repo: repo, 124 | headers: { 125 | authorization: `bearer ${core.getInput("github_token")}`, 126 | }, 127 | // We can remove this when update Node.js to v18 or upper. 128 | request: { 129 | fetch, 130 | }, 131 | }; 132 | 133 | let releases: Release[] = []; 134 | const newReleases: Release[] = []; 135 | 136 | const availableVersion = toSemver(this.availableVersion) ?? this.availableVersion; 137 | 138 | const cachePath = path.join(TEMP_PATH, `release-cache-${repo}.json`); 139 | let cacheKey = ""; 140 | 141 | fetching: 142 | for (;;) { 143 | const {repository}: DeepPartial = await graphql(RELEASES_QUERY, parameters); 144 | const resReleases = (repository?.releases?.edges?.map(node => node.node) || []) as Release[]; 145 | 146 | // First time. 147 | if (newReleases.length === 0) { 148 | const newestRelease = resReleases[0]; 149 | if (newestRelease == null) { 150 | return []; 151 | } 152 | cacheKey = `releases-${this.repository}-${newestRelease.updatedAt}`; 153 | core.debug(`[releases] Cache key: ${cacheKey}`); 154 | const hitCacheKey = await cache.restoreCache([cachePath], cacheKey, [`releases-${this.repository}-`]); 155 | const cacheHit = hitCacheKey === cacheKey; 156 | core.debug(`[releases] Cache ${cacheHit ? "" : "not "}hit`); 157 | // Complete cache exists: `cacheExists == true` 158 | // Incomplete cache exists: `cacheExists == false` but the cache is restored. 159 | // Cache does not exist: `cacheExists == false` and the cache is not restored. 160 | if (fs.existsSync(cachePath)) { 161 | releases = JSON.parse(fs.readFileSync(cachePath, {encoding: "utf8"})) as Release[]; 162 | core.debug(`[releases] Cache load: ${releases.length}`); 163 | core.debug(`[releases] Head of cache: ${JSON.stringify(releases[0])}`); 164 | } 165 | 166 | if (cacheHit) { 167 | return releases; 168 | } 169 | } 170 | 171 | for (const release of resReleases) { 172 | if (release.releaseAssets.edges.length === 0) { 173 | continue; 174 | } 175 | if (releases[0]?.tagName === release.tagName) { 176 | if (releases[0]?.tagCommit.oid === release.tagCommit?.oid) { 177 | // Reach to the head of incomplete cache. 178 | break fetching; 179 | } 180 | // if A.tagCommit.oid != B.tagCommit.oid thouth A.tagName == B.tagName, 181 | // presume that the tag commit was updated and so remove the old release. 182 | // e.g. "nightly" and "stable" tags 183 | releases.shift(); 184 | } 185 | newReleases.push(release); 186 | if (availableVersion) { 187 | if (availableVersion instanceof semver.SemVer) { 188 | if ((toSemver(release.tagName)?.compare(availableVersion) ?? 1) <= 0) { 189 | break fetching; 190 | } 191 | } else if (release.tagName === availableVersion) { 192 | break fetching; 193 | } 194 | } 195 | } 196 | const pageInfo = repository?.releases?.pageInfo; 197 | if (!pageInfo?.hasNextPage) { 198 | break; 199 | } 200 | parameters.cursor = pageInfo?.endCursor; 201 | } 202 | 203 | releases = newReleases.concat(releases); 204 | fs.writeFileSync(cachePath, JSON.stringify(releases)); 205 | try { 206 | core.debug(`[releases] Save the cache: ${releases.length}`); 207 | await cache.saveCache([cachePath], cacheKey); 208 | } catch (e) { 209 | if (e instanceof cache.ReserveCacheError) { 210 | core.error(`Error while caching releases: ${e.name}: ${e.message}`); 211 | } else { 212 | throw e; 213 | } 214 | } 215 | 216 | return releases; 217 | } 218 | 219 | private findRelease(vimVersion: string): Release | undefined { 220 | const isHead = vimVersion === "head"; 221 | if (isHead) { 222 | return this.releases[0]; 223 | } 224 | 225 | const isLatest = vimVersion === "latest"; 226 | if (isLatest) { 227 | return this.releases.find(release => release.isLatest); 228 | } 229 | 230 | const vimSemVer = toSemver(vimVersion); 231 | if (!vimSemVer) { 232 | return this.releases.find(release => release.tagName === vimVersion); 233 | } 234 | 235 | const releases = this.releases.filter((release) => { 236 | const releaseVersion = this.toSemverString(release); 237 | const releaseSemver = toSemver(releaseVersion); 238 | return releaseSemver && semver.lte(vimSemVer, releaseSemver); 239 | }); 240 | return releases.pop(); 241 | } 242 | 243 | private perpetuateVersion(): string { 244 | const release = this.release; 245 | if (!release) { 246 | throw new ActionError("Target release not found"); 247 | } 248 | 249 | const version = this.toSemverString(release); 250 | if (toSemver(version)) { 251 | return version; 252 | } 253 | 254 | // We assume not a semver tag is a symbolized tag (e.g. "stable", "nightly") 255 | const targetSha = release.tagCommit.oid; 256 | 257 | // It may be released as numbered version. 258 | // Only check the first page 259 | for (const release of this.releases.slice(0, 10)) { 260 | const {tagName} = release; 261 | if (!toSemver(tagName)) { 262 | continue; 263 | } 264 | const sha = release.tagCommit.oid; 265 | if (sha === targetSha) { 266 | return tagName; 267 | } 268 | } 269 | // Fallback: treats sha1 as version. 270 | return targetSha; 271 | } 272 | 273 | async downloadAsset(vimVersion: FixedVersion): Promise { 274 | const release = this.release; 275 | if (!release) { 276 | throw new ActionError(`Unknown version: ${vimVersion}`); 277 | } 278 | const releaseAssets = release.releaseAssets.edges.map(node => node.node); 279 | const asset = this.assetNamePatterns.map(pattern => releaseAssets.find(asset => pattern.test(asset.name))).find(v => v); 280 | if (!asset) { 281 | const assetNames = releaseAssets.map(asset => asset.name); 282 | throw new ActionError(`Target asset not found: /${this.assetNamePatterns.map(p => p.source).join("|")}/ in ${JSON.stringify(assetNames)}`); 283 | } 284 | const url = asset.downloadUrl; 285 | const dest = path.join(TEMP_PATH, this.repository, vimVersion, asset.name); 286 | return await downloadTool(url, dest); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/installer/semver_releases_installer.ts: -------------------------------------------------------------------------------- 1 | import {ReleasesInstaller, Release, toSemver} from "./releases_installer"; 2 | 3 | export abstract class SemverReleasesInstaller extends ReleasesInstaller { 4 | toSemverString(release: Release): string { 5 | return release.tagName; 6 | } 7 | 8 | canInstall(version: string): boolean { 9 | const ver = toSemver(version); 10 | const avail = toSemver(this.availableVersion); 11 | return !ver || !avail || ver.compare(avail) >= 0; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/installer/unix_vim_build_installer.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import {exec} from "@actions/exec"; 3 | import {FixedVersion, Installer} from "../interfaces"; 4 | import {VimBuildInstaller} from "./vim_build_installer"; 5 | import {backportPatch} from "../patch"; 6 | import * as semver from "semver"; 7 | 8 | export class UnixVimBuildInstaller extends VimBuildInstaller implements Installer { 9 | async install(vimVersion: FixedVersion): Promise { 10 | const reposPath = await this.cloneVim(vimVersion); 11 | await backportPatch(reposPath, vimVersion, this.isGUI); 12 | 13 | const args = [`--prefix=${this.installDir}`, "--with-features=huge"]; 14 | 15 | const vimSemver = semver.coerce(vimVersion, {loose: true}); 16 | 17 | if (process.platform === "darwin") { 18 | // To avoid `sed: RE error: illegal byte sequence` error, should set 'LC_ALL=C'. 19 | process.env.LC_ALL = "C"; 20 | 21 | if (vimSemver && semver.lt(vimSemver, "8.2.5135", true)) { 22 | args.push("CFLAGS=-Wno-implicit-int"); 23 | } 24 | } 25 | 26 | if (this.isGUI) { 27 | const [guiarg, guipkg] = 28 | vimSemver && semver.lt(vimSemver, "7.4.1402", true) 29 | ? ["gtk2", "libgtk2.0-dev"] 30 | : ["gtk3", "libgtk-3-dev"]; 31 | await exec("sudo", ["apt-get", "update"]); 32 | await exec("sudo", ["apt-get", "install", "libxmu-dev", "libxpm-dev", guipkg]); 33 | args.push(`--enable-gui=${guiarg}`, "--enable-fail-if-missing"); 34 | } 35 | await exec("./configure", args, {cwd: reposPath}); 36 | await exec("make", [], {cwd: reposPath}); 37 | await exec("make", ["install"], {cwd: reposPath}); 38 | } 39 | 40 | getPath(): string { 41 | return path.join(this.installDir, "bin"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/installer/vim_build_installer.ts: -------------------------------------------------------------------------------- 1 | import {BuildInstaller} from "./build_installer"; 2 | import {FixedVersion} from "../interfaces"; 3 | 4 | export abstract class VimBuildInstaller extends BuildInstaller { 5 | readonly repository = "vim/vim"; 6 | 7 | getExecutableName(): string { 8 | return this.isGUI ? "gvim" : "vim"; 9 | } 10 | 11 | abstract install(vimVersion: FixedVersion): Promise; 12 | abstract getPath(vimVersion: FixedVersion): string; 13 | } 14 | -------------------------------------------------------------------------------- /src/installer/windows_neovim_build_installer.ts: -------------------------------------------------------------------------------- 1 | import {exec} from "@actions/exec"; 2 | import {FixedVersion} from "../interfaces"; 3 | import {NeovimBuildInstaller} from "./neovim_build_installer"; 4 | 5 | export class WindowsNeovimBuildInstaller extends NeovimBuildInstaller { 6 | async install(vimVersion: FixedVersion): Promise { 7 | const reposPath = await this.cloneVim(vimVersion); 8 | await exec( 9 | "powershell.exe", 10 | ["ci\\build.ps1", "-NoTests"], 11 | {cwd: reposPath, env: {CONFIGURATION: "MSVC_64"}}, 12 | ); 13 | await exec("make", ["install"], {cwd: reposPath}); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/installer/windows_neovim_releases_installer.ts: -------------------------------------------------------------------------------- 1 | import {NeovimReleasesInstaller} from "./neovim_releases_installer"; 2 | 3 | export class WindowsNeovimReleasesInstaller extends NeovimReleasesInstaller { 4 | readonly assetNamePatterns: RegExp[] = [/^nvim-win64\.zip$/]; 5 | 6 | getExecutableName(): string { 7 | return this.isGUI ? "nvim-qt" : "nvim"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/installer/windows_vim_build_installer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import * as core from "@actions/core"; 4 | import {exec} from "@actions/exec"; 5 | import * as io from "@actions/io"; 6 | import {backportPatch} from "../patch"; 7 | import {FixedVersion} from "../interfaces"; 8 | import {VimBuildInstaller} from "./vim_build_installer"; 9 | 10 | export class WindowsVimBuildInstaller extends VimBuildInstaller { 11 | async install(vimVersion: FixedVersion): Promise { 12 | const reposPath = await this.cloneVim(vimVersion); 13 | const extraArgs = await backportPatch(reposPath, vimVersion, this.isGUI); 14 | const arch = core.getInput("arch").includes("64") ? "x64" : "x86"; 15 | const srcPath = path.join(reposPath, "src"); 16 | const batPath = path.join(srcPath, "install.bat"); 17 | const guiOptions = this.isGUI ? "GUI=yes OLE=yes DIRECTX=yes" : "GUI=no OLE=no DIRECTX=no"; 18 | const vsPath = await this.getVSPath(); 19 | 20 | fs.writeFileSync(batPath, ` 21 | call "${path.join(vsPath, "VC\\Auxiliary\\Build\\vcvarsall.bat")}" ${arch} 22 | 23 | rem Suppress progress animation 24 | sed -e "s/@<<$/@<< | sed -e 's#.*\\\\r.*##'/" Make_mvc.mak > Make_mvc2.mak 25 | 26 | nmake -nologo -f Make_mvc2.mak ${guiOptions} FEATURES=HUGE IME=yes MBYTE=yes ICONV=yes DEBUG=no TERMINAL=yes ${extraArgs.join(" ")} 27 | 28 | copy /Y ..\\README.txt ..\\runtime 29 | copy /Y ..\\vimtutor.bat ..\\runtime 30 | copy /Y *.exe ..\\runtime 31 | copy /Y tee\\*.exe ..\\runtime 32 | copy /Y xxd\\*.exe ..\\runtime 33 | `); 34 | await exec("cmd.exe", ["/c", batPath], {cwd: srcPath}); 35 | await io.mkdirP(this.installDir); 36 | const runtime = path.join(reposPath, "runtime"); 37 | await io.cp(runtime, this.getPath(vimVersion), {recursive: true}); 38 | } 39 | 40 | getPath(vimVersion: FixedVersion): string { 41 | const matched = /^v(\d+)\.(\d+)/.exec(vimVersion); 42 | const vimDir = matched ? `vim${matched[1]}${matched[2]}` : "runtime"; 43 | return path.join(this.installDir, vimDir); 44 | } 45 | 46 | async getVSPath(): Promise { 47 | let vspath = ""; 48 | const options = { 49 | listeners: { 50 | stdout: (data: Buffer) => { vspath += data.toString(); } 51 | } 52 | }; 53 | await exec("vswhere", ["-products", "*", "-latest", "-property", "installationPath"], options); 54 | return vspath.trim(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/installer/windows_vim_releases_installer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import * as core from "@actions/core"; 4 | import {extractZip} from "@actions/tool-cache"; 5 | import {SemverReleasesInstaller} from "./semver_releases_installer"; 6 | import {ActionError} from "../action_error"; 7 | import {FixedVersion} from "../interfaces"; 8 | import {TEMP_PATH} from "../temp"; 9 | 10 | export class WindowsVimReleasesInstaller extends SemverReleasesInstaller { 11 | readonly repository: string = "vim/vim-win32-installer"; 12 | readonly arch = core.getInput("arch").includes("64") ? "x64" : "x86"; 13 | readonly assetNamePatterns: RegExp[] = [RegExp(String.raw`^gvim_.*_${this.arch}(?:_signed)?\.zip$`)]; 14 | readonly availableVersion: string = "v8.0.0"; 15 | 16 | getExecutableName(): string { 17 | return this.isGUI ? "gvim" : "vim"; 18 | } 19 | 20 | async install(vimVersion: FixedVersion): Promise { 21 | const archiveFilePath = await this.downloadAsset(vimVersion); 22 | const tmpDir = path.join(TEMP_PATH, "tmpinst"); 23 | fs.mkdirSync(tmpDir, {recursive: true}); 24 | 25 | await extractZip(archiveFilePath, tmpDir); 26 | 27 | const candidates = [ 28 | "", 29 | path.join("vim", this.vimDir(vimVersion)), 30 | ]; 31 | const targetDir = this.findExecutable(tmpDir, candidates); 32 | 33 | const installPath = this.getPath(vimVersion); 34 | fs.renameSync(targetDir, installPath); 35 | 36 | fs.rmSync(tmpDir, {recursive: true}); 37 | } 38 | 39 | getPath(vimVersion: FixedVersion): string { 40 | const vimDir = this.vimDir(vimVersion); 41 | return path.join(this.installDir, vimDir); 42 | } 43 | 44 | private vimDir(vimVersion: FixedVersion): string { 45 | const matched = /^v(\d+)\.(\d+)/.exec(vimVersion); 46 | return matched ? `vim${matched[1]}${matched[2]}` : "runtime"; 47 | } 48 | 49 | private findExecutable(basePath: string, candidates: string[]): string { 50 | const executable = `${this.getExecutableName()}.exe`; 51 | for (const candidate of candidates) { 52 | const target = path.join(basePath, candidate); 53 | if (fs.existsSync(path.join(target, executable))) { 54 | return target; 55 | } 56 | } 57 | throw new ActionError("Installed executable not found"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | enum FixedVersionBrand {} 2 | 3 | export type FixedVersion = FixedVersionBrand & string; 4 | 5 | export interface Installer { 6 | readonly installType: InstallType; 7 | readonly installDir: string; 8 | readonly isGUI: boolean; 9 | getExecutableName(): string; 10 | canInstall(vimVersion: string): boolean; 11 | resolveVersion(vimVersion: string): Promise; 12 | install(vimVersion: FixedVersion): Promise; 13 | getPath(vimVersion: FixedVersion): string; 14 | } 15 | 16 | export const InstallType = { 17 | build: "build", 18 | download: "download", 19 | } as const; 20 | export type InstallType = typeof InstallType[keyof typeof InstallType]; 21 | 22 | export const VimType = { 23 | vim: "vim", 24 | neovim: "neovim", 25 | macvim: "macvim", 26 | } as const; 27 | export type VimType = typeof VimType[keyof typeof VimType]; 28 | 29 | export function isVimType(maybeVimType: string): maybeVimType is VimType { 30 | return 0 <= Object.keys(VimType).indexOf(maybeVimType); 31 | } 32 | -------------------------------------------------------------------------------- /src/patch.ts: -------------------------------------------------------------------------------- 1 | import * as semver from "semver"; 2 | import {Buffer} from "buffer"; 3 | import {exec} from "@actions/exec"; 4 | 5 | export async function backportPatch(reposPath: string, vimVersion: string, isGUI: boolean): Promise { 6 | const extraArgs: string[] = []; 7 | const vimSemver = semver.coerce(vimVersion, {loose: true}); 8 | if (!vimSemver) { 9 | return extraArgs; 10 | } 11 | 12 | if (process.platform === "win32") { 13 | extraArgs.push(...await backportPatchForWindows(reposPath, vimSemver, vimVersion, isGUI)); 14 | } 15 | if (process.platform === "darwin") { 16 | await backportPatchForMacOS(reposPath, vimSemver); 17 | } 18 | return extraArgs; 19 | } 20 | 21 | export async function backportPatchForWindows(reposPath: string, vimVersion: semver.SemVer, vimVersionString: string, isGUI: boolean): Promise { 22 | const extraArgs: string[] = []; 23 | if (semver.lt(vimVersion, "7.4.960")) { 24 | // Apply all patches before 7.4.960 for Make_mvc.mak because cherry-picking fails. 25 | await exec("sh", ["-c", `curl -s https://github.com/vim/vim/compare/${vimVersionString}...v7.4.960.diff | git apply --include src/Make_mvc.mak`], {cwd: reposPath}); 26 | 27 | if (semver.lt(vimVersion, "7.4.399")) { 28 | // After patch 7.4.399, `crypt` files are separeted. 29 | await exec("sed", ["-i", "/crypt/d", "src/Make_mvc.mak"], {cwd: reposPath}); 30 | } 31 | } 32 | 33 | if (isGUI && semver.lt(vimVersion, "7.4.1944")) { 34 | const baseVersion = semver.lt(vimVersion, "7.4.960") ? "v7.4.960" : vimVersionString; 35 | // Apply all patches before 7.4.1944 for Make_mvc.mak because cherry-picking fails. 36 | await exec("sh", ["-c", `curl -s https://github.com/vim/vim/compare/${baseVersion}...v7.4.1944.diff | git apply --include src/Make_mvc.mak`], {cwd: reposPath}); 37 | await exec("curl", ["-sL", "https://github.com/vim/vim/raw/refs/tags/v7.4.1944/src/xpm/x64/lib-vc14/libXpm.lib", "-o", "src/xpm/x64/lib-vc14/libXpm.lib", "--create-dirs"], {cwd: reposPath}); 38 | await exec("curl", ["-sL", "https://github.com/vim/vim/raw/refs/tags/v7.4.1944/src/xpm/x86/lib-vc14/libXpm.lib", "-o", "src/xpm/x86/lib-vc14/libXpm.lib", "--create-dirs"], {cwd: reposPath}); 39 | 40 | if (semver.lt(vimVersion, "7.4.393")) { 41 | // After patch 7.4.393, directx related files are added. 42 | await exec("sed", ["-i", "/gui_dwrite/d", "src/Make_mvc.mak"], {cwd: reposPath}); 43 | } 44 | if (semver.lt(vimVersion, "7.4.1040")) { 45 | // tee.exe is not supported before 7.4.1040. 46 | await exec("sed", ["-i", "/tee\\\\.exe \\\\\\\\$/d", "src/Make_mvc.mak"], {cwd: reposPath}); 47 | } 48 | if (semver.lt(vimVersion, "7.4.1154")) { 49 | // After patch 7.4.1154, `json` support is added. 50 | await exec("sed", ["-i", "/json/d", "src/Make_mvc.mak"], {cwd: reposPath}); 51 | } 52 | if (semver.lt(vimVersion, "7.4.1169")) { 53 | // Patches about channel is applied but it is not available before 7.4.1169. 54 | extraArgs.push("CHANNEL=no"); 55 | } 56 | } 57 | 58 | if (semver.lt(vimVersion, "8.0.881")) { 59 | // Apply patch 8.0.0881. 60 | // We use Vim to apply this patch because it is difficult with `git apply` or `sed`. 61 | const script =` 62 | :e src/GvimExt/Makefile 63 | /^!include 64 | :.-1,.d 65 | :i 66 | !elseif "$(USE_WIN32MAK)"=="yes" 67 | !include 68 | !else 69 | cc = cl 70 | link = link 71 | rc = rc 72 | cflags = -nologo -c 73 | lflags = -incremental:no -nologo 74 | rcflags = /r 75 | olelibsdll = ole32.lib uuid.lib oleaut32.lib user32.lib gdi32.lib advapi32.lib 76 | . 77 | :w 78 | :e src/Make_mvc.mak 79 | /^!include 80 | :.-1,.d 81 | :i 82 | !elseif "$(USE_WIN32MAK)"=="yes" 83 | !include 84 | !else 85 | link = link 86 | . 87 | :wq 88 | `; 89 | await exec("vim", ["-es"], {cwd: reposPath, input: Buffer.from(script), ignoreReturnCode: true}); 90 | } 91 | return extraArgs; 92 | } 93 | 94 | export async function backportPatchForMacOS(reposPath: string, vimSemver: semver.SemVer): Promise { 95 | if (semver.lt(vimSemver, "7.4.55")) { 96 | // To avoid `conflicting types for 'sigaltstack'` 97 | await exec("sh", ["-c", "curl -s https://github.com/vim/vim/compare/v7.4.054...v7.4.055.diff | git apply --exclude src/version.c"], {cwd: reposPath}); 98 | } 99 | 100 | if (semver.lt(vimSemver, "7.4.1648")) { 101 | // Vim crashes causes by `Trace/BPT trap: 5` on MacOS. 102 | // Apply patch 7.4.1648. 103 | // Cannot use `git apply` because of context mismatch. 104 | await exec("sed", ["-i", "", "-e", "/#define VV_NAME/s/, {0}$//", "-e", "/static struct vimvar/,+4s/dictitem_T/dictitem16_T/;/char[[:blank:]]*vv_filler.*/d", "src/eval.c"], {cwd: reposPath}); 105 | await exec("sed", ["-i", "", "/typedef struct dictitem_S dictitem_T;/a\\\nstruct dictitem16_S {\\\n typval_T di_tv;\\\n char_u di_flags;\\\n char_u di_key[17];\\\n};\\\ntypedef struct dictitem16_S dictitem16_T;", "src/structs.h"], {cwd: reposPath}); 106 | } 107 | 108 | if (semver.lt(vimSemver, "8.2.1119")) { 109 | // Workaround: 110 | // Building Vim before v8.2.1119 on MacOS will fail because default Xcode was changed to 12. 111 | // https://github.com/actions/virtual-environments/commit/c09dca28df69d9aaaeac5635257d23722810d307#diff-7a1606bd717fc0cf55f9419157117d9ca306f91bd2fdfc294720687d7be1b2c7R220 112 | // 113 | // We should apply patch v8.2.1119 to src/auto/configure. 114 | const patch = ` 115 | --- a/src/auto/configure 116 | +++ b/src/auto/configure 117 | @@ -14143,8 +14143,8 @@ else 118 | main() { 119 | uint32_t nr1 = (uint32_t)-1; 120 | uint32_t nr2 = (uint32_t)0xffffffffUL; 121 | - if (sizeof(uint32_t) != 4 || nr1 != 0xffffffffUL || nr2 + 1 != 0) exit(1); 122 | - exit(0); 123 | + if (sizeof(uint32_t) != 4 || nr1 != 0xffffffffUL || nr2 + 1 != 0) return 1; 124 | + return 0; 125 | } 126 | _ACEOF 127 | if ac_fn_c_try_run "$LINENO"; then : 128 | `.trim() + "\n"; 129 | await exec("patch", ["-p1"], {cwd: reposPath, input: Buffer.from(patch)}); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/temp.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | function tempPath(): string { 4 | const home = process.env.HOME || process.env.USERPROFILE; 5 | if (home) { 6 | return path.join(home, "tmp"); 7 | } 8 | throw new Error("$HOME could not detect."); 9 | } 10 | 11 | export const TEMP_PATH = process.env["RUNNER_TEMP"] || tempPath(); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib", 4 | "module": "node16", 5 | "moduleResolution": "node16", 6 | "lib": ["ES2023"], 7 | "strict": true, 8 | "preserveConstEnums": true, 9 | "noImplicitAny": true, 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "noEmitOnError": true, 15 | "strictNullChecks": true, 16 | "target": "es2022", 17 | "sourceMap": true, 18 | "esModuleInterop": true, 19 | "resolveJsonModule": true 20 | }, 21 | "include": [ 22 | "src/**/*.ts" 23 | ] 24 | } 25 | --------------------------------------------------------------------------------