├── .gitattributes ├── .prettierrc.yml ├── src ├── install-state.ts ├── cache-state.ts ├── properties.json ├── post.ts ├── index.ts ├── cache-save.ts ├── cache-save.unit.test.ts ├── cache-restore.ts ├── cache-restore.unit.test.ts ├── post.unit.test.ts ├── script.ts ├── install.ts ├── script.unit.test.ts ├── mpm.ts ├── install.unit.test.ts ├── mpm.unit.test.ts ├── matlab.ts └── matlab.unit.test.ts ├── jest.config.js ├── .prettierignore ├── SECURITY.md ├── tsconfig.json ├── action.yml ├── .eslintrc.js ├── devel └── contributing.md ├── package.json ├── license.txt ├── .gitignore ├── .github └── workflows │ ├── publish.yml │ └── bat.yml └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/* binary 2 | lib/* binary 3 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | tabWidth: 4 2 | printWidth: 100 3 | overrides: 4 | - files: "*.yml" 5 | options: 6 | tabWidth: 2 7 | -------------------------------------------------------------------------------- /src/install-state.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The MathWorks, Inc. 2 | 3 | export enum State { 4 | InstallSuccessful = 'MATLAB_INSTALL_SUCCESSFUL', 5 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | testRunner: "jest-circus/runner", 5 | collectCoverage: true, 6 | }; 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | ## No need to work on final dist 2 | dist 3 | 4 | ## Ignore external node_modules 5 | node_modules 6 | 7 | ## Don't include intermediate TypeScript compilation 8 | tsout 9 | 10 | ## Ignore code coverage 11 | coverage 12 | 13 | ## Editor related 14 | .vscode 15 | -------------------------------------------------------------------------------- /src/cache-state.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | export enum State { 4 | CachePrimaryKey = 'MATLAB_CACHE_KEY', 5 | CacheMatchedKey = 'MATLAB_CACHE_RESULT', 6 | MatlabCachePath = 'MATLAB_CACHE_PATH', 7 | SupportPackagesCachePath = 'MATLAB_SUPPORT_FILES_CACHE_PATH', 8 | } 9 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Vulnerabilities 2 | 3 | If you believe you have discovered a security vulnerability, please report it to 4 | [security@mathworks.com](mailto:security@mathworks.com). Please see 5 | [MathWorks Vulnerability Disclosure Policy for Security Researchers](https://www.mathworks.com/company/aboutus/policies_statements/vulnerability-disclosure-policy.html) 6 | for additional information. 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "CommonJS", 5 | "sourceMap": true, 6 | "rootDir": "src", 7 | "outDir": "lib", 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true 12 | }, 13 | 14 | "exclude": ["node_modules", "lib", "**/*.test.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /src/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "matlabDepsUrl": "https://ssd.mathworks.com/supportfiles/ci/matlab-deps/v0/install.sh", 3 | "matlabReleaseInfoUrl": "https://ssd.mathworks.com/supportfiles/ci/matlab-release/v0/", 4 | "matlabBatchRootUrl": "https://ssd.mathworks.com/supportfiles/ci/matlab-batch/v1/", 5 | "mpmRootUrl": "https://www.mathworks.com/mpm/", 6 | "appleSiliconJdkUrl": "https://corretto.aws/downloads/resources/8.402.08.1/amazon-corretto-8.402.08.1-macosx-aarch64.pkg" 7 | } 8 | -------------------------------------------------------------------------------- /src/post.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 The MathWorks, Inc. 2 | 3 | import * as core from "@actions/core"; 4 | import { cacheMATLAB } from "./cache-save"; 5 | import { State } from './install-state'; 6 | 7 | export async function run() { 8 | const cache = core.getBooleanInput('cache'); 9 | const installSuccessful = core.getState(State.InstallSuccessful); 10 | if (cache && installSuccessful === 'true') { 11 | await cacheMATLAB(); 12 | } 13 | } 14 | 15 | run().catch((e) => { 16 | core.error(e); 17 | }); -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The MathWorks, Inc. 2 | 3 | import * as core from "@actions/core"; 4 | import * as install from "./install"; 5 | 6 | /** 7 | * Gather action inputs and then run action. 8 | */ 9 | export async function run() { 10 | const platform = process.platform; 11 | const architecture = process.arch; 12 | const release = core.getInput("release"); 13 | const products = core.getMultilineInput("products"); 14 | const cache = core.getBooleanInput("cache"); 15 | return install.install(platform, architecture, release, products, cache); 16 | } 17 | 18 | run().catch((e) => { 19 | core.setFailed(e); 20 | }); 21 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020-2025 The MathWorks, Inc. 2 | 3 | name: Setup MATLAB 4 | description: >- 5 | Set up a specific version of MATLAB 6 | inputs: 7 | release: 8 | description: >- 9 | MATLAB release to set up (R2021a or later) 10 | required: false 11 | default: latest 12 | products: 13 | description: >- 14 | Products to set up in addition to MATLAB, specified as a list of product names separated by spaces 15 | required: false 16 | default: "" 17 | cache: 18 | description: >- 19 | Option to enable caching with GitHub Actions, specified as false or true 20 | required: false 21 | default: false 22 | outputs: 23 | matlabroot: 24 | description: >- 25 | A full path to the folder where MATLAB is installed 26 | runs: 27 | using: node20 28 | main: dist/setup/index.js 29 | post: dist/cache-save/index.js 30 | post-if: always() 31 | -------------------------------------------------------------------------------- /src/cache-save.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import * as core from '@actions/core'; 4 | import * as cache from '@actions/cache'; 5 | import {State} from './cache-state'; 6 | 7 | export async function cacheMATLAB() { 8 | const matchedKey = core.getState(State.CacheMatchedKey); 9 | const primaryKey = core.getState(State.CachePrimaryKey); 10 | const matlabPath = core.getState(State.MatlabCachePath); 11 | const supportPackagesPath = core.getState(State.SupportPackagesCachePath); 12 | 13 | 14 | if (primaryKey === matchedKey) { 15 | core.info(`Cache hit occurred for key: ${primaryKey}, not saving cache.`); 16 | return; 17 | } 18 | 19 | try { 20 | await cache.saveCache([matlabPath, supportPackagesPath], primaryKey); 21 | core.info(`Cache saved with the key: ${primaryKey}`); 22 | } catch (e) { 23 | core.warning(`Failed to save MATLAB to cache: ${e}`); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "overrides": [ 11 | { 12 | "env": { 13 | "node": true 14 | }, 15 | "files": [ 16 | ".eslintrc.{js,cjs}" 17 | ], 18 | "parserOptions": { 19 | "sourceType": "script" 20 | } 21 | }, 22 | { 23 | "files": [ 24 | "**/*.ts" 25 | ], 26 | "rules": { 27 | "prefer-const": "off" 28 | } 29 | } 30 | ], 31 | "parser": "@typescript-eslint/parser", 32 | "parserOptions": { 33 | "ecmaVersion": "latest", 34 | "sourceType": "module" 35 | }, 36 | "plugins": [ 37 | "@typescript-eslint" 38 | ], 39 | "rules": { 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /devel/contributing.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Verify changes by running tests and building locally with the following command: 4 | 5 | ``` 6 | npm run ci 7 | ``` 8 | 9 | ## Creating a New Release 10 | 11 | Familiarize yourself with the best practices for [releasing and maintaining GitHub actions](https://docs.github.com/en/actions/creating-actions/releasing-and-maintaining-actions). 12 | 13 | Changes should be made on a new branch. The new branch should be merged to the main branch via a pull request. Ensure that all of the CI pipeline checks and tests have passed for your changes. 14 | 15 | After the pull request has been approved and merged to main, follow the Github process for [creating a new release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository). The release must follow semantic versioning (ex: vX.Y.Z). This will kick off a new pipeline execution, and the action will automatically be published to the GitHub Actions Marketplace if the pipeline finishes successfully. Check the [GitHub Marketplace](https://github.com/marketplace/actions/setup-matlab) and check the major version in the repository (ex: v1 for v1.0.0) to ensure that the new semantically versioned tag is available. 16 | -------------------------------------------------------------------------------- /src/cache-save.unit.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import * as core from '@actions/core'; 4 | import * as cache from '@actions/cache'; 5 | import { cacheMATLAB } from './cache-save'; 6 | 7 | jest.mock("@actions/cache"); 8 | jest.mock("@actions/core"); 9 | 10 | afterEach(() => { 11 | jest.resetAllMocks(); 12 | }); 13 | 14 | describe("cache-save", () => { 15 | let saveCacheMock: jest.Mock; 16 | let getStateMock: jest.Mock; 17 | 18 | beforeEach(() => { 19 | saveCacheMock = cache.saveCache as jest.Mock; 20 | getStateMock = core.getState as jest.Mock; 21 | }); 22 | 23 | it("saves cache if key does not equal matched key", async () => { 24 | getStateMock.mockReturnValueOnce("matched-key").mockReturnValueOnce("primary-key") 25 | await expect(cacheMATLAB()).resolves.toBeUndefined(); 26 | expect(saveCacheMock).toHaveBeenCalledTimes(1); 27 | }); 28 | 29 | it("does not re-save cache if key equals matched key", async () => { 30 | getStateMock.mockReturnValueOnce("cache-key").mockReturnValueOnce("cache-key") 31 | await expect(cacheMATLAB()).resolves.toBeUndefined(); 32 | expect(saveCacheMock).toHaveBeenCalledTimes(0); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/cache-restore.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import * as cache from '@actions/cache'; 4 | import * as core from '@actions/core'; 5 | import * as crypto from "crypto"; 6 | import { State } from './cache-state'; 7 | import { Release } from './matlab'; 8 | 9 | export async function restoreMATLAB(release: Release, platform: string, architecture: string, products: string[], matlabPath: string, supportPackagesPath?: string): Promise { 10 | const installHash = crypto.createHash('sha256').update(products.sort().join('|')).digest('hex'); 11 | const keyPrefix = `matlab-cache-${platform}-${architecture}-${release.version}`; 12 | const primaryKey = `${keyPrefix}-${installHash}`; 13 | const cachePaths = [matlabPath]; 14 | if (supportPackagesPath) { 15 | cachePaths.push(supportPackagesPath); 16 | } 17 | const cacheKey: string | undefined = await cache.restoreCache(cachePaths, primaryKey); 18 | 19 | core.saveState(State.CachePrimaryKey, primaryKey); 20 | core.saveState(State.MatlabCachePath, matlabPath); 21 | core.saveState(State.SupportPackagesCachePath, supportPackagesPath); 22 | 23 | if (!cacheKey) { 24 | core.info(`${keyPrefix} cache is not found`); 25 | return false; 26 | } 27 | 28 | core.saveState(State.CacheMatchedKey, cacheKey); 29 | core.info(`Cache restored from key: ${cacheKey}`); 30 | return true 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setup-matlab-action", 3 | "author": "The MathWorks, Inc.", 4 | "version": "2.6.1", 5 | "description": "", 6 | "main": "lib/index.js", 7 | "scripts": { 8 | "clean": "rm -rf dist lib", 9 | "format": "prettier --write .", 10 | "format-check": "prettier --check .", 11 | "lint": "eslint \"**/*.ts\" --fix", 12 | "package": "ncc build src/index.ts -o dist/setup --minify && ncc build src/post.ts -o dist/cache-save --minify", 13 | "test": "jest", 14 | "all": "npm run lint && npm test && npm run package", 15 | "ci": "npm run clean && npm ci && npm run all" 16 | }, 17 | "files": [ 18 | "lib/" 19 | ], 20 | "dependencies": { 21 | "@actions/cache": "^4.0.5", 22 | "@actions/core": "^1.11.1", 23 | "@actions/exec": "^1.1.1", 24 | "@actions/http-client": "^2.2.3", 25 | "@actions/io": "^1.1.3", 26 | "@actions/tool-cache": "^2.0.2" 27 | }, 28 | "devDependencies": { 29 | "@types/jest": "^30.0.0", 30 | "@types/node": "^24.3.0", 31 | "@typescript-eslint/eslint-plugin": "^8.40.0", 32 | "@typescript-eslint/parser": "^8.40.0", 33 | "@vercel/ncc": "^0.38.3", 34 | "eslint": "^8.57.1", 35 | "jest": "^30.0.5", 36 | "jest-circus": "^30.0.5", 37 | "prettier": "3.6.2", 38 | "ts-jest": "^29.4.1", 39 | "typescript": "^5.9.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/cache-restore.unit.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 The MathWorks, Inc. 2 | 3 | import * as cache from "@actions/cache"; 4 | import * as core from "@actions/core"; 5 | import { restoreMATLAB } from "./cache-restore"; 6 | 7 | jest.mock("@actions/cache"); 8 | jest.mock("@actions/core"); 9 | 10 | afterEach(() => { 11 | jest.resetAllMocks(); 12 | }); 13 | 14 | describe("cache-restore", () => { 15 | let restoreCacheMock: jest.Mock; 16 | let saveStateMock: jest.Mock; 17 | 18 | const platform = "linux"; 19 | const arch = "x64"; 20 | const release = { 21 | name: "r2022b", 22 | version: "9.13.0", 23 | update: "latest", 24 | isPrerelease: false, 25 | }; 26 | const products = ["MATLAB", "Parallel_Computing_Toolbox"]; 27 | const location = "/path/to/matlab"; 28 | 29 | beforeEach(() => { 30 | restoreCacheMock = cache.restoreCache as jest.Mock; 31 | saveStateMock = core.saveState as jest.Mock; 32 | }); 33 | 34 | it("returns true if cache is found", async () => { 35 | restoreCacheMock.mockReturnValue("matched-cache-key"); 36 | await expect(restoreMATLAB(release, platform, arch, products, location)).resolves.toBe(true); 37 | expect(saveStateMock).toHaveBeenCalledTimes(4); 38 | }); 39 | 40 | 41 | it("returns false if cache is not found", async () => { 42 | await expect(restoreMATLAB(release, platform, arch, products, location)).resolves.toBe(false); 43 | expect(saveStateMock).toHaveBeenCalledTimes(3); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 The MathWorks, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors 14 | may be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /src/post.unit.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2025 The MathWorks, Inc. 2 | 3 | import * as core from '@actions/core'; 4 | import { cacheMATLAB } from "./cache-save"; 5 | import { run } from "./post"; 6 | 7 | jest.mock("@actions/core"); 8 | jest.mock("./cache-save"); 9 | 10 | afterEach(() => { 11 | jest.resetAllMocks(); 12 | }); 13 | 14 | describe("post", () => { 15 | let getBooleanInputMock: jest.Mock; 16 | let getStateMock: jest.Mock; 17 | let cacheMATLABMock: jest.Mock; 18 | 19 | beforeEach(() => { 20 | getBooleanInputMock = core.getBooleanInput as jest.Mock; 21 | getStateMock = core.getState as jest.Mock; 22 | cacheMATLABMock = cacheMATLAB as jest.Mock; 23 | }); 24 | 25 | it("caches MATLAB when cache true and install successful", async () => { 26 | getBooleanInputMock.mockReturnValueOnce(true); 27 | getStateMock.mockReturnValueOnce('true'); 28 | await expect(run()).resolves.toBeUndefined(); 29 | expect(cacheMATLABMock).toHaveBeenCalledTimes(1); 30 | }); 31 | 32 | it("does not cache MATLAB when cache false", async () => { 33 | getBooleanInputMock.mockReturnValueOnce(false); 34 | getStateMock.mockReturnValueOnce('true'); 35 | await expect(run()).resolves.toBeUndefined(); 36 | expect(cacheMATLABMock).toHaveBeenCalledTimes(0); 37 | }); 38 | 39 | it("does not cache MATLAB when install not successful", async () => { 40 | getBooleanInputMock.mockReturnValueOnce(true); 41 | await expect(run()).resolves.toBeUndefined(); 42 | expect(cacheMATLABMock).toHaveBeenCalledTimes(0); 43 | }); 44 | }); -------------------------------------------------------------------------------- /src/script.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The MathWorks, Inc. 2 | 3 | import * as exec from "@actions/exec"; 4 | import * as io from "@actions/io"; 5 | import * as tc from "@actions/tool-cache"; 6 | import path from "path"; 7 | 8 | /** 9 | * Download and run a script on the runner. 10 | * 11 | * @param platform Operating system of the runner (e.g., "win32" or "linux"). 12 | * @param url URL of the script to run. 13 | * @param args Arguments to pass to the script. 14 | */ 15 | export async function downloadAndRunScript(platform: string, url: string, args?: string[]) { 16 | const scriptPath = await tc.downloadTool(url); 17 | const cmd = await generateExecCommand(platform, scriptPath); 18 | 19 | const exitCode = await exec.exec(cmd, args); 20 | 21 | if (exitCode !== 0) { 22 | return Promise.reject(Error(`Script exited with non-zero code ${exitCode}`)); 23 | } 24 | return; 25 | } 26 | 27 | /** 28 | * Generate platform-specific command to run a script. 29 | * 30 | * @param platform Operating system of the runner (e.g. "win32" or "linux"). 31 | * @param scriptPath Path to the script (on runner's filesystem). 32 | */ 33 | export async function generateExecCommand(platform: string, scriptPath: string): Promise { 34 | // Run the install script using bash 35 | let installCmd = `bash ${scriptPath}`; 36 | 37 | if (platform !== "win32") { 38 | const sudo = await io.which("sudo"); 39 | if (sudo) { 40 | installCmd = `sudo -E ${installCmd}`; 41 | } 42 | } 43 | 44 | return installCmd; 45 | } 46 | 47 | export function defaultInstallRoot(platform: string, programName: string): string { 48 | let installRoot: string; 49 | if (platform === "win32") { 50 | installRoot = path.join("C:","Program Files", programName); 51 | } else { 52 | installRoot = path.join("/","opt", programName); 53 | } 54 | return installRoot; 55 | } 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## For development environment 2 | .vscode 3 | 4 | # Don't include intermediate TypeScript compilation; 5 | # it should be forcibly added on release 6 | lib 7 | 8 | # Leave out dist; it should be forcibly added on release 9 | dist 10 | 11 | ## https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | lerna-debug.log* 19 | 20 | # Diagnostic reports (https://nodejs.org/api/report.html) 21 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 22 | 23 | # Runtime data 24 | pids 25 | *.pid 26 | *.seed 27 | *.pid.lock 28 | 29 | # Directory for instrumented libs generated by jscoverage/JSCover 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | coverage 34 | *.lcov 35 | 36 | # nyc test coverage 37 | .nyc_output 38 | 39 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 40 | .grunt 41 | 42 | # Bower dependency directory (https://bower.io/) 43 | bower_components 44 | 45 | # node-waf configuration 46 | .lock-wscript 47 | 48 | # Compiled binary addons (https://nodejs.org/api/addons.html) 49 | build/Release 50 | 51 | # Dependency directories 52 | node_modules/ 53 | jspm_packages/ 54 | 55 | # Snowpack dependency directory (https://snowpack.dev/) 56 | web_modules/ 57 | 58 | # TypeScript cache 59 | *.tsbuildinfo 60 | 61 | # Optional npm cache directory 62 | .npm 63 | 64 | # Optional eslint cache 65 | .eslintcache 66 | 67 | # Microbundle cache 68 | .rpt2_cache/ 69 | .rts2_cache_cjs/ 70 | .rts2_cache_es/ 71 | .rts2_cache_umd/ 72 | 73 | # Optional REPL history 74 | .node_repl_history 75 | 76 | # Output of 'npm pack' 77 | *.tgz 78 | 79 | # Yarn Integrity file 80 | .yarn-integrity 81 | 82 | # dotenv environment variables file 83 | .env 84 | .env.test 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | .parcel-cache 89 | 90 | # Next.js build output 91 | .next 92 | out 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | # dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | # yarn v2 123 | .yarn/cache 124 | .yarn/unplugged 125 | .yarn/build-state.yml 126 | .yarn/install-state.gz 127 | .pnp.* 128 | -------------------------------------------------------------------------------- /src/install.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2025 The MathWorks, Inc. 2 | 3 | import * as core from "@actions/core"; 4 | import * as matlab from "./matlab"; 5 | import * as mpm from "./mpm"; 6 | import * as path from "path"; 7 | import * as cache from "./cache-restore"; 8 | import { State } from './install-state'; 9 | 10 | /** 11 | * Set up an instance of MATLAB on the runner. 12 | * 13 | * First, system dependencies are installed. Then the ephemeral installer script 14 | * is invoked. 15 | * 16 | * @param platform Operating system of the runner (e.g. "win32" or "linux"). 17 | * @param architecture Architecture of the runner (e.g. "x64", or "x86"). 18 | * @param release Release of MATLAB to be set up (e.g. "latest" or "R2020a"). 19 | * @param products A list of products to install (e.g. ["MATLAB", "Simulink"]). 20 | * @param useCache whether to use the cache to restore & save the MATLAB installation 21 | */ 22 | export async function install(platform: string, architecture: string, release: string, products: string[], useCache: boolean) { 23 | const releaseInfo = await matlab.getReleaseInfo(release); 24 | if (releaseInfo.name < "r2020b") { 25 | return Promise.reject(Error(`Release '${releaseInfo.name}' is not supported. Use 'R2020b' or a later release.`)); 26 | } 27 | 28 | // Install system dependencies if cloud-hosted 29 | if (process.env["RUNNER_ENVIRONMENT"] === "github-hosted" && process.env["AGENT_ISSELFHOSTED"] !== "1") { 30 | await core.group("Preparing system for MATLAB", async () => { 31 | await matlab.installSystemDependencies(platform, architecture, releaseInfo.name); 32 | }); 33 | } 34 | 35 | await core.group("Setting up MATLAB", async () => { 36 | let matlabArch = architecture; 37 | if (platform === "darwin" && architecture === "arm64" && releaseInfo.name < "r2023b") { 38 | matlabArch = "x64"; 39 | } 40 | 41 | let [destination]: [string, boolean] = await matlab.getToolcacheDir(platform, releaseInfo); 42 | let cacheHit = false; 43 | 44 | if (useCache) { 45 | const supportFilesDir = matlab.getSupportPackagesPath(platform, releaseInfo.name); 46 | cacheHit = await cache.restoreMATLAB(releaseInfo, platform, matlabArch, products, destination, supportFilesDir); 47 | } 48 | 49 | if (!cacheHit) { 50 | const mpmPath: string = await mpm.setup(platform, matlabArch); 51 | await mpm.install(mpmPath, releaseInfo, products, destination); 52 | core.saveState(State.InstallSuccessful, 'true'); 53 | } 54 | 55 | core.addPath(path.join(destination, "bin")); 56 | core.setOutput('matlabroot', destination); 57 | 58 | await matlab.setupBatch(platform, matlabArch); 59 | 60 | if (platform === "win32") { 61 | if (matlabArch === "x86") { 62 | core.addPath(path.join(destination, "runtime", "win32")); 63 | } else { 64 | core.addPath(path.join(destination, "runtime", "win64")); 65 | } 66 | } 67 | }); 68 | 69 | return; 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | release: 4 | types: published 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | outputs: 13 | tag: ${{ steps.update-package-version.outputs.version }} 14 | steps: 15 | - uses: actions/checkout@v5 16 | - name: Configure git 17 | run: | 18 | git config user.name 'Release Action' 19 | git config user.email '<>' 20 | - uses: actions/setup-node@v5 21 | with: 22 | node-version: 20 23 | 24 | # Call `npm version`. It increments the version and commits the changes. 25 | # We'll save the output (new version string) for use in the following 26 | # steps 27 | - name: Update package version 28 | id: update-package-version 29 | run: | 30 | git tag -d "${{ github.event.release.tag_name }}" 31 | VERSION=$(npm version "${{ github.event.release.tag_name }}" --no-git-tag-version) 32 | git add package.json package-lock.json 33 | git commit -m "[skip ci] Bump $VERSION" 34 | git push origin HEAD:main 35 | 36 | # Now carry on, business as usual 37 | - name: Perform npm tasks 38 | run: npm run ci 39 | 40 | # Finally, create a detached commit containing the built artifacts and tag 41 | # it with the release. Note: the fact that the branch is locally updated 42 | # will not be relayed (pushed) to origin 43 | - name: Commit to release branch 44 | id: release_info 45 | run: | 46 | # Check for semantic versioning 47 | longVersion="${{github.event.release.tag_name}}" 48 | echo "Preparing release for version $longVersion" 49 | [[ $longVersion == v[0-9]*.[0-9]*.[0-9]* ]] || (echo "must follow semantic versioning" && exit 1) 50 | majorVersion=$(echo ${longVersion%.*.*}) 51 | minorVersion=$(echo ${longVersion%.*}) 52 | 53 | # Add the built artifacts. Using --force because dist should be in 54 | # .gitignore 55 | git add --force dist 56 | 57 | # Make the commit 58 | MESSAGE="Build for $(git rev-parse --short HEAD)" 59 | git commit --allow-empty -m "$MESSAGE" 60 | git tag -f -a -m "Release $longVersion" $longVersion 61 | 62 | # Get the commit of the tag you just released 63 | commitHash=$(git rev-list -n 1 $longVersion) 64 | 65 | # Delete the old major and minor version tags locally 66 | git tag -d $majorVersion || true 67 | git tag -d $minorVersion || true 68 | 69 | # Make new major and minor version tags locally that point to the commit you got from the "git rev-list" above 70 | git tag -f $majorVersion $commitHash 71 | git tag -f $minorVersion $commitHash 72 | 73 | # Force push the new minor version tag to overwrite the old tag remotely 74 | echo "Pushing new tags" 75 | git push -f origin $longVersion 76 | git push -f origin $majorVersion 77 | git push -f origin $minorVersion 78 | -------------------------------------------------------------------------------- /src/script.unit.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2024 The MathWorks, Inc. 2 | 3 | import * as exec from "@actions/exec"; 4 | import * as io from "@actions/io"; 5 | import * as toolCache from "@actions/tool-cache"; 6 | import * as script from "./script"; 7 | 8 | jest.mock("@actions/exec"); 9 | jest.mock("@actions/io"); 10 | jest.mock("@actions/tool-cache"); 11 | 12 | afterEach(() => { 13 | jest.resetAllMocks(); 14 | }); 15 | 16 | describe("script downloader/runner", () => { 17 | const downloadToolMock = toolCache.downloadTool as jest.Mock; 18 | const execMock = exec.exec as jest.Mock; 19 | 20 | const sampleUrl = "https://www.mathworks.com/"; 21 | const samplePlatform = "linux"; 22 | const doDownloadAndRunScript = () => script.downloadAndRunScript(samplePlatform, sampleUrl); 23 | 24 | it("ideally works", async () => { 25 | downloadToolMock.mockResolvedValue("nice"); 26 | execMock.mockResolvedValue(0); 27 | 28 | await expect(doDownloadAndRunScript()).resolves.not.toThrow(); 29 | expect(downloadToolMock).toHaveBeenCalledTimes(1); 30 | expect(execMock).toHaveBeenCalledTimes(1); 31 | }); 32 | 33 | it("rejects when toolCache.downloadTool() fails", async () => { 34 | downloadToolMock.mockRejectedValue(new Error("failed")); 35 | 36 | await expect(script.downloadAndRunScript(samplePlatform, sampleUrl)).rejects.toBeDefined(); 37 | expect(downloadToolMock).toHaveBeenCalledTimes(1); 38 | expect(execMock).not.toHaveBeenCalled(); 39 | }); 40 | 41 | it("rejects when the downloaded script fails", async () => { 42 | downloadToolMock.mockResolvedValue("nice"); 43 | execMock.mockRejectedValue(new Error("oof")); 44 | 45 | await expect(script.downloadAndRunScript(samplePlatform, sampleUrl)).rejects.toBeDefined(); 46 | expect(downloadToolMock).toHaveBeenCalledTimes(1); 47 | expect(execMock).toHaveBeenCalledTimes(1); 48 | }); 49 | 50 | it("rejects when the downloaded script exits with non-zero code", async () => { 51 | downloadToolMock.mockResolvedValue("nice"); 52 | execMock.mockResolvedValue(1); 53 | 54 | await expect(script.downloadAndRunScript(samplePlatform, sampleUrl)).rejects.toBeDefined(); 55 | expect(downloadToolMock).toHaveBeenCalledTimes(1); 56 | expect(execMock).toHaveBeenCalledTimes(1); 57 | }); 58 | }); 59 | 60 | describe("install command generator", () => { 61 | const whichMock = io.which as jest.Mock; 62 | 63 | const scriptPath = "hello.sh"; 64 | 65 | beforeAll(() => { 66 | jest.restoreAllMocks(); 67 | }); 68 | 69 | it("does not change the command on Windows", () => { 70 | const cmd = script.generateExecCommand("win32", scriptPath); 71 | expect(cmd).resolves.toEqual(`bash ${scriptPath}`); 72 | }); 73 | 74 | ["darwin", "linux"].forEach((platform) => { 75 | it(`calls the command with sudo on ${platform}`, () => { 76 | whichMock.mockResolvedValue("path/to/sudo"); 77 | const cmd = script.generateExecCommand(platform, scriptPath); 78 | expect(cmd).resolves.toEqual(`sudo -E bash ${scriptPath}`); 79 | }); 80 | 81 | it(`calls the command without sudo on ${platform}`, () => { 82 | whichMock.mockResolvedValue(""); 83 | const cmd = script.generateExecCommand(platform, scriptPath); 84 | expect(cmd).resolves.toEqual(`bash ${scriptPath}`); 85 | }); 86 | }); 87 | }); 88 | 89 | describe("default install root", () => { 90 | const testCase = (platform: string, subdirectory: string) => { 91 | it(`sets correct install directory for ${platform}`, async () => { 92 | const installDir = script.defaultInstallRoot(platform, "matlab-batch"); 93 | expect(installDir).toContain(subdirectory); 94 | expect(installDir).toContain("matlab-batch") 95 | }) 96 | }; 97 | 98 | testCase("win32", 'Program Files'); 99 | testCase("darwin", "opt"); 100 | testCase("linux", "opt"); 101 | }) 102 | -------------------------------------------------------------------------------- /src/mpm.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The MathWorks, Inc. 2 | 3 | import * as exec from "@actions/exec"; 4 | import * as tc from "@actions/tool-cache"; 5 | import {rmRF} from "@actions/io"; 6 | import * as path from "path"; 7 | import * as fs from 'fs'; 8 | import * as matlab from "./matlab"; 9 | import properties from "./properties.json"; 10 | 11 | export async function setup(platform: string, architecture: string): Promise { 12 | let mpmUrl: string; 13 | let ext = ""; 14 | if (architecture != "x64" && !(platform == "darwin" && architecture == "arm64")) { 15 | return Promise.reject(Error(`This action is not supported on ${platform} runners using the ${architecture} architecture.`)); 16 | } 17 | switch (platform) { 18 | case "win32": 19 | mpmUrl = properties.mpmRootUrl + "win64/mpm"; 20 | ext = ".exe"; 21 | break; 22 | case "linux": 23 | mpmUrl = properties.mpmRootUrl + "glnxa64/mpm"; 24 | break; 25 | case "darwin": 26 | if (architecture == "x64") { 27 | mpmUrl = properties.mpmRootUrl + "maci64/mpm"; 28 | } else { 29 | mpmUrl = properties.mpmRootUrl + "maca64/mpm"; 30 | } 31 | break; 32 | default: 33 | return Promise.reject(Error(`This action is not supported on ${platform} runners using the ${architecture} architecture.`)); 34 | } 35 | 36 | let runner_temp = process.env["RUNNER_TEMP"] 37 | if (!runner_temp) { 38 | return Promise.reject(Error("Unable to find runner temporary directory.")); 39 | } 40 | let mpmDest = path.join(runner_temp, `mpm${ext}`); 41 | 42 | // Delete mpm file if it exists 43 | if (fs.existsSync(mpmDest)) { 44 | try { 45 | fs.unlinkSync(mpmDest); 46 | } catch (err) { 47 | return Promise.reject(Error(`Failed to delete existing mpm file: ${err}`)); 48 | } 49 | } 50 | 51 | let mpm: string = await tc.downloadTool(mpmUrl, mpmDest); 52 | 53 | if (platform !== "win32") { 54 | const exitCode = await exec.exec(`chmod +x "${mpm}"`); 55 | if (exitCode !== 0) { 56 | return Promise.reject(Error("Unable to set up mpm.")); 57 | } 58 | } 59 | return mpm 60 | } 61 | 62 | export async function install(mpmPath: string, release: matlab.Release, products: string[], destination: string) { 63 | const mpmRelease = release.name + release.update 64 | // remove spaces and flatten product list 65 | let parsedProducts = products.flatMap(p => p.split(/[ ]+/)); 66 | // Add MATLAB by default 67 | parsedProducts.push("MATLAB"); 68 | // Remove duplicate products 69 | parsedProducts = [...new Set(parsedProducts)]; 70 | 71 | let mpmArguments: string[] = [ 72 | "install", 73 | `--release=${mpmRelease}`, 74 | `--destination=${destination}`, 75 | ] 76 | if (release.isPrerelease) { 77 | mpmArguments = mpmArguments.concat(["--release-status=Prerelease"]); 78 | } 79 | mpmArguments = mpmArguments.concat("--products", ...parsedProducts); 80 | 81 | let output = ""; 82 | const options = { 83 | listeners: { 84 | stdout: (data: Buffer) => { 85 | const text = data.toString(); 86 | output += text; 87 | process.stdout.write(text); 88 | }, 89 | stderr: (data: Buffer) => { 90 | const text = data.toString(); 91 | output += text; 92 | process.stderr.write(text); 93 | }, 94 | }, 95 | ignoreReturnCode: true, 96 | silent: true, 97 | }; 98 | 99 | const exitCode = await exec.exec(mpmPath, mpmArguments, options).catch(async e => { 100 | await rmRF(destination); 101 | throw e; 102 | }); 103 | 104 | if (exitCode !== 0 && !output.toLowerCase().includes("already installed")) { 105 | await rmRF(destination); 106 | return Promise.reject(Error(`Script exited with non-zero code ${exitCode}`)); 107 | } 108 | 109 | return; 110 | } 111 | -------------------------------------------------------------------------------- /.github/workflows/bat.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: [push] 3 | permissions: 4 | contents: read 5 | 6 | jobs: 7 | bat: 8 | name: Build and Test 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v5 12 | - uses: actions/setup-node@v5 13 | with: 14 | node-version: 20 15 | - name: Perform npm tasks 16 | run: npm run ci 17 | - uses: actions/upload-artifact@v4 18 | with: 19 | name: built-action 20 | path: | 21 | **/* 22 | !node_modules/ 23 | 24 | integ: 25 | needs: bat 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | include: 31 | - os: ubuntu-latest 32 | release: latest 33 | products: Symbolic_Math_Toolbox 34 | check-matlab: matlabVer = ver('matlab'); assert(~isempty(matlabVer)); 35 | check-toolbox: symbolicVer = ver('symbolic'); assert(~isempty(symbolicVer)); 36 | - os: ubuntu-latest 37 | release: latest-including-prerelease 38 | products: Symbolic_Math_Toolbox 39 | check-matlab: matlabVer = ver('matlab'); assert(~isempty(matlabVer)); 40 | check-toolbox: symbolicVer = ver('symbolic'); assert(~isempty(symbolicVer)); 41 | - os: ubuntu-22.04 42 | release: R2021bU2 43 | products: | 44 | MATLAB 45 | Symbolic_Math_Toolbox 46 | check-matlab: matlabVer = ver('matlab'); assert(strcmp(matlabVer.Release,'(R2021b)')); 47 | check-toolbox: symbolicVer = ver('symbolic'); assert(strcmp(symbolicVer.Release,'(R2021b)')); 48 | - os: windows-latest 49 | release: latest 50 | products: Symbolic_Math_Toolbox 51 | check-matlab: matlabVer = ver('matlab'); assert(~isempty(matlabVer)); 52 | check-toolbox: symbolicVer = ver('symbolic'); assert(~isempty(symbolicVer)); 53 | # Added pauses in the check commands on macos until g3258674 is fixed 54 | - os: macos-latest 55 | release: latest 56 | products: Symbolic_Math_Toolbox 57 | check-matlab: pause(10); matlabVer = ver('matlab'); assert(~isempty(matlabVer)); 58 | check-toolbox: pause(10); symbolicVer = ver('symbolic'); assert(~isempty(symbolicVer)); 59 | - os: macos-14 60 | release: latest 61 | products: Symbolic_Math_Toolbox 62 | check-matlab: pause(10); matlabVer = ver('matlab'); assert(~isempty(matlabVer)); 63 | check-toolbox: pause(10); symbolicVer = ver('symbolic'); assert(~isempty(symbolicVer)); 64 | - os: macos-14 65 | release: R2023a 66 | products: Symbolic_Math_Toolbox 67 | check-matlab: matlabVer = ver('matlab'); assert(strcmp(matlabVer.Release,'(R2023a)')); 68 | check-toolbox: symbolicVer = ver('symbolic'); assert(strcmp(symbolicVer.Release,'(R2023a)')); 69 | steps: 70 | - uses: actions/download-artifact@v5 71 | with: 72 | name: built-action 73 | - name: Install selected products 74 | id: setup_matlab 75 | uses: ./ 76 | with: 77 | release: ${{ matrix.release }} 78 | products: ${{ matrix.products }} 79 | - name: Check matlabroot output is set 80 | run: 'if [[ "${{ steps.setup_matlab.outputs.matlabroot }}" != *"MATLAB"* ]]; then exit 1; fi' 81 | shell: bash 82 | - name: Check MATLAB version 83 | uses: matlab-actions/run-command@v2 84 | with: 85 | command: "${{ matrix.check-matlab }}" 86 | - name: Check toolbox version 87 | uses: matlab-actions/run-command@v2 88 | with: 89 | command: "${{ matrix.check-toolbox }}" 90 | - name: Check NoOp on 2nd install 91 | uses: ./ 92 | with: 93 | release: ${{ matrix.release }} 94 | products: ${{ matrix.products }} 95 | - name: Install additional products 96 | if: matrix.os != 'windows-latest' 97 | uses: ./ 98 | with: 99 | release: ${{ matrix.release }} 100 | products: Image_Processing_Toolbox 101 | - name: Check additional product was installed 102 | if: matrix.os != 'windows-latest' 103 | uses: matlab-actions/run-command@v2 104 | with: 105 | command: assert(any(strcmp({ver().Name},'Image Processing Toolbox'))) 106 | - name: Call setup MATLAB again with different release # should not error as in issue 130 107 | uses: ./ 108 | with: 109 | release: R2023b 110 | - name: Check MATLAB version 111 | uses: matlab-actions/run-command@v2 112 | with: 113 | command: matlabVer = ver('matlab'); assert(strcmp(matlabVer.Release,'(R2023b)')); 114 | 115 | -------------------------------------------------------------------------------- /src/install.unit.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-2025 The MathWorks, Inc. 2 | 3 | import * as core from "@actions/core"; 4 | import * as cache from "./cache-restore"; 5 | import * as install from "./install"; 6 | import * as matlab from "./matlab"; 7 | import * as mpm from "./mpm"; 8 | import { State } from './install-state'; 9 | 10 | jest.mock("@actions/core"); 11 | jest.mock("./matlab"); 12 | jest.mock("./mpm"); 13 | jest.mock("./cache-restore"); 14 | 15 | afterEach(() => { 16 | jest.resetAllMocks(); 17 | }); 18 | 19 | describe("install procedure", () => { 20 | let matlabInstallSystemDependenciesMock: jest.Mock; 21 | let matlabGetReleaseInfoMock: jest.Mock; 22 | let matlabGetToolcacheDirMock: jest.Mock; 23 | let matlabSetupBatchMock: jest.Mock; 24 | let mpmSetupMock: jest.Mock; 25 | let mpmInstallMock: jest.Mock; 26 | let saveStateMock: jest.Mock; 27 | let addPathMock: jest.Mock; 28 | let setOutputMock: jest.Mock; 29 | let restoreMATLABMock: jest.Mock; 30 | 31 | const runnerEnv = "github-hosted"; 32 | const agentIsSelfHosted = "0"; 33 | 34 | const platform = "linux"; 35 | const arch = "x64"; 36 | const release = "latest"; 37 | const releaseInfo = { 38 | name: "r2022b", 39 | version: "9.13.0", 40 | updateNumber: "latest" 41 | }; 42 | const products = ["MATLAB", "Parallel_Computing_Toolbox"]; 43 | const useCache = false; 44 | 45 | const doInstall = () => install.install(platform, arch, release, products, useCache); 46 | 47 | beforeEach(() => { 48 | matlabInstallSystemDependenciesMock = matlab.installSystemDependencies as jest.Mock; 49 | matlabGetReleaseInfoMock = matlab.getReleaseInfo as jest.Mock; 50 | matlabGetToolcacheDirMock = matlab.getToolcacheDir as jest.Mock; 51 | matlabSetupBatchMock = matlab.setupBatch as jest.Mock; 52 | mpmSetupMock = mpm.setup as jest.Mock; 53 | mpmInstallMock = mpm.install as jest.Mock; 54 | saveStateMock = core.saveState as jest.Mock; 55 | addPathMock = core.addPath as jest.Mock; 56 | setOutputMock = core.setOutput as jest.Mock; 57 | restoreMATLABMock = cache.restoreMATLAB as jest.Mock; 58 | 59 | // Mock core.group to simply return the output of the func it gets from 60 | // the caller 61 | (core.group as jest.Mock).mockImplementation(async (_, func) => { 62 | return func(); 63 | }); 64 | matlabGetReleaseInfoMock.mockResolvedValue(releaseInfo); 65 | matlabGetToolcacheDirMock.mockResolvedValue(["/opt/hostedtoolcache/MATLAB/9.13.0/x64", false]); 66 | 67 | process.env["RUNNER_ENVIRONMENT"] = runnerEnv; 68 | process.env["AGENT_ISSELFHOSTED"] = agentIsSelfHosted; 69 | }); 70 | 71 | it("ideally works", async () => { 72 | await expect(doInstall()).resolves.toBeUndefined(); 73 | expect(matlabInstallSystemDependenciesMock).toHaveBeenCalledTimes(1); 74 | expect(matlabSetupBatchMock).toHaveBeenCalledTimes(1); 75 | expect(mpmSetupMock).toHaveBeenCalledTimes(1); 76 | expect(mpmInstallMock).toHaveBeenCalledTimes(1); 77 | expect(saveStateMock).toHaveBeenCalledWith(State.InstallSuccessful, 'true'); 78 | expect(addPathMock).toHaveBeenCalledTimes(1); 79 | expect(setOutputMock).toHaveBeenCalledTimes(1); 80 | }); 81 | 82 | it("re-calls MPM install even if MATLAB already exists in toolcache", async () => { 83 | matlabGetToolcacheDirMock.mockResolvedValue(["/opt/hostedtoolcache/MATLAB/9.13.0/x64", true]); 84 | await expect(doInstall()).resolves.toBeUndefined(); 85 | expect(mpmInstallMock).toHaveBeenCalledTimes(1); 86 | expect(saveStateMock).toHaveBeenCalledTimes(1); 87 | expect(addPathMock).toHaveBeenCalledTimes(1); 88 | expect(setOutputMock).toHaveBeenCalledTimes(1); 89 | }); 90 | 91 | it("rejects for unsupported MATLAB release", async () => { 92 | matlabGetReleaseInfoMock.mockResolvedValue({ 93 | name: "r2020a", 94 | version: "9.8.0", 95 | updateNumber: "latest" 96 | }); 97 | await expect(install.install(platform, arch, "r2020a", products, useCache)).rejects.toBeDefined(); 98 | }); 99 | 100 | it("rejects for invalid MATLAB version", async () => { 101 | matlabGetReleaseInfoMock.mockRejectedValue(Error("oof")); 102 | await expect(doInstall()).rejects.toBeDefined(); 103 | }); 104 | 105 | it("sets up dependencies for github-hosted runners", async () => { 106 | await doInstall(); 107 | expect(matlabInstallSystemDependenciesMock).toHaveBeenCalled(); 108 | }); 109 | 110 | it("does not set up dependencies for self-hosted runners", async () => { 111 | process.env["RUNNER_ENVIRONMENT"] = "self-hosted"; 112 | await doInstall(); 113 | expect(matlabInstallSystemDependenciesMock).not.toHaveBeenCalled(); 114 | }); 115 | 116 | it("rejects when the setup deps fails", async () => { 117 | matlabInstallSystemDependenciesMock.mockRejectedValueOnce(Error("oof")); 118 | await expect(doInstall()).rejects.toBeDefined(); 119 | }); 120 | 121 | it("rejects when the mpm install fails", async () => { 122 | mpmInstallMock.mockRejectedValue(Error("oof")); 123 | await expect(doInstall()).rejects.toBeDefined(); 124 | expect(saveStateMock).toHaveBeenCalledTimes(0); 125 | }); 126 | 127 | it("rejects when the matlab-batch install fails", async () => { 128 | matlabSetupBatchMock.mockRejectedValueOnce(Error("oof")); 129 | await expect(doInstall()).rejects.toBeDefined(); 130 | }); 131 | 132 | it("Does not restore cache if useCache is false", async () => { 133 | await expect(doInstall()).resolves.toBeUndefined(); 134 | expect(restoreMATLABMock).toHaveBeenCalledTimes(0); 135 | expect(mpmSetupMock).toHaveBeenCalledTimes(1); 136 | expect(mpmInstallMock).toHaveBeenCalledTimes(1); 137 | }); 138 | 139 | it("Does not install if useCache is true and there is cache hit", async () => { 140 | restoreMATLABMock.mockResolvedValue(true); 141 | await expect(install.install(platform, arch, release, products, true)).resolves.toBeUndefined(); 142 | expect(restoreMATLABMock).toHaveBeenCalledTimes(1); 143 | expect(mpmSetupMock).toHaveBeenCalledTimes(0); 144 | expect(mpmInstallMock).toHaveBeenCalledTimes(0); 145 | }); 146 | 147 | it("Does install if useCache is true and there is no cache hit", async () => { 148 | restoreMATLABMock.mockResolvedValue(false); 149 | await expect(install.install(platform, arch, release, products, true)).resolves.toBeUndefined(); 150 | expect(restoreMATLABMock).toHaveBeenCalledTimes(1); 151 | expect(mpmSetupMock).toHaveBeenCalledTimes(1); 152 | expect(mpmInstallMock).toHaveBeenCalledTimes(1); 153 | }); 154 | 155 | it("installs Intel version on Apple silicon prior to R2023b", async () => { 156 | matlabGetReleaseInfoMock.mockResolvedValue({ 157 | name: "r2023a", 158 | version: "9.14.0", 159 | updateNumber: "latest" 160 | }); 161 | await expect(install.install("darwin", "arm64", "r2023a", products, true)).resolves.toBeUndefined(); 162 | expect(matlabInstallSystemDependenciesMock).toHaveBeenCalledWith("darwin", "arm64", "r2023a"); 163 | expect(matlabSetupBatchMock).toHaveBeenCalledWith("darwin", "x64"); 164 | expect(mpmSetupMock).toHaveBeenCalledWith("darwin", "x64"); 165 | }); 166 | 167 | it("adds runtime path for Windows platform", async () => { 168 | await expect(install.install("win32", arch, release, products, useCache)).resolves.toBeUndefined(); 169 | expect(addPathMock).toHaveBeenCalledTimes(2); 170 | expect(addPathMock).toHaveBeenCalledWith(expect.stringContaining("bin")); 171 | expect(addPathMock).toHaveBeenCalledWith(expect.stringContaining("runtime")); 172 | }); 173 | 174 | }); 175 | -------------------------------------------------------------------------------- /src/mpm.unit.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The MathWorks, Inc. 2 | 3 | import * as exec from "@actions/exec"; 4 | import * as tc from "@actions/tool-cache"; 5 | import * as io from "@actions/io"; 6 | import * as path from "path"; 7 | import * as mpm from "./mpm"; 8 | import * as script from "./script"; 9 | 10 | jest.mock("@actions/core"); 11 | jest.mock("@actions/exec"); 12 | jest.mock("@actions/tool-cache"); 13 | jest.mock("@actions/io"); 14 | jest.mock("./script"); 15 | 16 | afterEach(() => { 17 | jest.resetAllMocks(); 18 | }); 19 | 20 | describe("setup mpm", () => { 21 | let tcDownloadToolMock: jest.Mock; 22 | let tcCacheFileMock: jest.Mock; 23 | let execMock: jest.Mock; 24 | let defaultInstallRootMock: jest.Mock; 25 | const arch = "x64"; 26 | const mpmMockPath = path.join("path", "to", "mpm"); 27 | 28 | beforeEach(() => { 29 | tcDownloadToolMock = tc.downloadTool as jest.Mock; 30 | tcCacheFileMock = tc.cacheFile as jest.Mock; 31 | execMock = exec.exec as jest.Mock; 32 | defaultInstallRootMock = script.defaultInstallRoot as jest.Mock; 33 | tcDownloadToolMock.mockResolvedValue(mpmMockPath); 34 | tcCacheFileMock.mockResolvedValue(mpmMockPath); 35 | process.env.RUNNER_TEMP = path.join("runner", "workdir", "tmp"); 36 | }); 37 | 38 | describe("test on all supported platforms", () => { 39 | it(`works on linux`, async () => { 40 | const platform = "linux"; 41 | execMock.mockResolvedValue(0); 42 | await expect(mpm.setup(platform, arch)).resolves.toBe(mpmMockPath); 43 | expect(tcDownloadToolMock.mock.calls[0][0]).toContain("glnxa64"); 44 | }); 45 | 46 | it(`works on windows`, async () => { 47 | const platform = "win32"; 48 | tcDownloadToolMock.mockResolvedValue(mpmMockPath); 49 | execMock.mockResolvedValue(0); 50 | await expect(mpm.setup(platform, arch)).resolves.toBe(path.join(mpmMockPath)); 51 | expect(tcDownloadToolMock.mock.calls[0][0]).toContain("win64"); 52 | }); 53 | 54 | it(`works on mac`, async () => { 55 | const platform = "darwin"; 56 | tcDownloadToolMock.mockResolvedValue(mpmMockPath); 57 | execMock.mockResolvedValue(0); 58 | await expect(mpm.setup(platform, arch)).resolves.toBe(path.join(mpmMockPath)); 59 | expect(tcDownloadToolMock.mock.calls[0][0]).toContain("maci64"); 60 | }); 61 | 62 | it(`works on mac with apple silicon`, async () => { 63 | const platform = "darwin"; 64 | execMock.mockResolvedValue(0); 65 | await expect(mpm.setup(platform, "arm64")).resolves.toBe(mpmMockPath); 66 | expect(tcDownloadToolMock.mock.calls[0][0]).toContain("maca64"); 67 | }); 68 | }); 69 | 70 | it("errors on unsupported platform", async () => { 71 | await expect(() => mpm.setup('sunos', arch)).rejects.toBeDefined(); 72 | }); 73 | 74 | it("errors on unsupported architecture", async () => { 75 | const platform = "linux"; 76 | await expect(() => mpm.setup(platform, 'x86')).rejects.toBeDefined(); 77 | }); 78 | 79 | it("errors without RUNNER_TEMP", async () => { 80 | const platform = "linux"; 81 | process.env.RUNNER_TEMP = ''; 82 | tcDownloadToolMock.mockResolvedValue(mpmMockPath); 83 | defaultInstallRootMock.mockReturnValue(path.join("path", "to", "install", "root")); 84 | execMock.mockResolvedValue(0); 85 | await expect(mpm.setup(platform, arch)).rejects.toBeDefined(); 86 | }); 87 | 88 | it("rejects when the download fails", async () => { 89 | const platform = "linux"; 90 | tcDownloadToolMock.mockRejectedValue(Error("oof")); 91 | execMock.mockResolvedValue(0); 92 | await expect(mpm.setup(platform, arch)).rejects.toBeDefined(); 93 | }); 94 | 95 | it("rejects when the chmod fails", async () => { 96 | const platform = "linux"; 97 | tcDownloadToolMock.mockResolvedValue("/path/to/mpm"); 98 | execMock.mockResolvedValue(1); 99 | await expect(mpm.setup(platform, arch)).rejects.toBeDefined(); 100 | }); 101 | 102 | }); 103 | 104 | describe("mpm install", () => { 105 | let execMock: jest.Mock; 106 | let rmRFMock: jest.Mock; 107 | const mpmPath = "mpm"; 108 | const releaseInfo = {name: "r2022b", version: "9.13.0", update: "", isPrerelease: false}; 109 | const mpmRelease = "r2022b"; 110 | beforeEach(() => { 111 | execMock = exec.exec as jest.Mock; 112 | rmRFMock = io.rmRF as jest.Mock; 113 | }); 114 | 115 | it("works with multiline products list", async () => { 116 | const destination ="/opt/matlab"; 117 | const products = ["MATLAB", "Compiler"]; 118 | const expectedMpmArgs = [ 119 | "install", 120 | `--release=${mpmRelease}`, 121 | `--destination=${destination}`, 122 | "--products", 123 | "MATLAB", 124 | "Compiler", 125 | ] 126 | execMock.mockResolvedValue(0); 127 | 128 | await expect(mpm.install(mpmPath, releaseInfo, products, destination)).resolves.toBeUndefined(); 129 | expect(execMock.mock.calls[0][1]).toMatchObject(expectedMpmArgs); 130 | }); 131 | 132 | it("works works with space separated products list", async () => { 133 | const destination = "/opt/matlab"; 134 | const products = ["MATLAB Compiler"]; 135 | const expectedMpmArgs = [ 136 | "install", 137 | `--release=${mpmRelease}`, 138 | `--destination=${destination}`, 139 | "--products", 140 | "MATLAB", 141 | "Compiler", 142 | ] 143 | execMock.mockResolvedValue(0); 144 | 145 | await expect(mpm.install(mpmPath, releaseInfo, products, destination)).resolves.toBeUndefined(); 146 | expect(execMock.mock.calls[0][1]).toMatchObject(expectedMpmArgs); 147 | }); 148 | 149 | it("works with prerelease", async () => { 150 | const prereleaseInfo = {name: "r2022b", version: "2022.2.999", update: "", isPrerelease: true}; 151 | const destination ="/opt/matlab"; 152 | const products = ["MATLAB", "Compiler"]; 153 | const expectedMpmArgs = [ 154 | "install", 155 | `--release=${mpmRelease}`, 156 | `--destination=${destination}`, 157 | "--release-status=Prerelease", 158 | "--products", 159 | "MATLAB", 160 | "Compiler", 161 | ] 162 | execMock.mockResolvedValue(0); 163 | 164 | await expect(mpm.install(mpmPath, prereleaseInfo, products, destination)).resolves.toBeUndefined(); 165 | expect(execMock.mock.calls[0][1]).toMatchObject(expectedMpmArgs); 166 | }); 167 | 168 | it("rejects and cleans on mpm rejection", async () => { 169 | const destination = "/opt/matlab"; 170 | const products = ["MATLAB", "Compiler"]; 171 | execMock.mockRejectedValue(1); 172 | await expect(mpm.install(mpmPath, releaseInfo, products, destination)).rejects.toBeDefined(); 173 | expect(rmRFMock).toHaveBeenCalledWith(destination); 174 | }); 175 | 176 | it("rejects and cleans on failed install", async () => { 177 | const destination = "/opt/matlab"; 178 | const products = ["MATLAB", "Compiler"]; 179 | execMock.mockResolvedValue(1); 180 | await expect(mpm.install(mpmPath, releaseInfo, products, destination)).rejects.toBeDefined(); 181 | expect(rmRFMock).toHaveBeenCalledWith(destination); 182 | }); 183 | 184 | it("does not reject when mpm exits non-zero but reports already installed", async () => { 185 | const destination = "/opt/matlab"; 186 | const products = ["MATLAB", "Compiler"]; 187 | 188 | // Simulate mpm writing the "already installed" message to stdout and returning non-zero 189 | execMock.mockImplementation((cmd: string, args: string[], options?: exec.ExecOptions) => { 190 | if (options && options.listeners && typeof options.listeners.stdout === 'function') { 191 | options.listeners.stdout(Buffer.from("All specified products are already installed.")); 192 | } 193 | return Promise.resolve(1); 194 | }); 195 | 196 | await expect(mpm.install(mpmPath, releaseInfo, products, destination)).resolves.toBeUndefined(); 197 | expect(rmRFMock).not.toHaveBeenCalled(); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /src/matlab.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The MathWorks, Inc. 2 | 3 | import * as core from "@actions/core"; 4 | import * as exec from "@actions/exec"; 5 | import * as http from "@actions/http-client"; 6 | import * as io from "@actions/io"; 7 | import * as tc from "@actions/tool-cache"; 8 | import * as fs from "fs"; 9 | import { homedir } from "os"; 10 | import * as path from "path"; 11 | import properties from "./properties.json"; 12 | import * as script from "./script"; 13 | 14 | export interface Release { 15 | name: string; 16 | version: string; 17 | update: string; 18 | isPrerelease: boolean; 19 | } 20 | 21 | export async function getToolcacheDir(platform: string, release: Release): Promise<[string, boolean]> { 22 | let toolpath: string = tc.find("MATLAB", release.version); 23 | let alreadyExists = false; 24 | if (toolpath) { 25 | core.info(`Found MATLAB ${release.name} in cache at ${toolpath}.`); 26 | alreadyExists = true; 27 | } else { 28 | toolpath = await makeToolcacheDir(platform, release); 29 | } 30 | if (platform == "darwin") { 31 | toolpath = toolpath + "/MATLAB.app"; 32 | } 33 | return [toolpath, alreadyExists] 34 | } 35 | 36 | async function makeToolcacheDir(platform: string, release: Release): Promise { 37 | let toolcacheDir: string; 38 | if (platform === "win32") { 39 | toolcacheDir = await makeWindowsHostedToolpath(release) 40 | .catch(async () => await makeDefaultToolpath(release)); 41 | } else { 42 | toolcacheDir = await makeDefaultToolpath(release); 43 | } 44 | return toolcacheDir; 45 | } 46 | 47 | async function makeWindowsHostedToolpath(release: Release): Promise { 48 | // bail early if not on a github hosted runner 49 | if (process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted' && process.env['AGENT_ISSELFHOSTED'] === '1') { 50 | return Promise.reject(); 51 | } 52 | 53 | const defaultToolCacheRoot = process.env['RUNNER_TOOL_CACHE']; 54 | if (!defaultToolCacheRoot) { 55 | return Promise.reject(); 56 | } 57 | 58 | // make sure runner has expected directory structure 59 | if (!fs.existsSync('d:\\') || !fs.existsSync('c:\\')) { 60 | return Promise.reject(); 61 | } 62 | 63 | const actualToolCacheRoot = defaultToolCacheRoot.replace("C:", "D:").replace("c:", "d:"); 64 | process.env['RUNNER_TOOL_CACHE'] = actualToolCacheRoot; 65 | 66 | try { 67 | // create install directory and link it to the toolcache directory 68 | fs.writeFileSync(".keep", ""); 69 | let actualToolCacheDir = await tc.cacheFile(".keep", ".keep", "MATLAB", release.version); 70 | await io.rmRF(".keep"); 71 | let defaultToolCacheDir = actualToolCacheDir.replace(actualToolCacheRoot, defaultToolCacheRoot); 72 | 73 | // remove cruft from incomplete installs 74 | await io.rmRF(defaultToolCacheDir); 75 | 76 | // link to actual tool cache directory 77 | fs.mkdirSync(path.dirname(defaultToolCacheDir), {recursive: true}); 78 | fs.symlinkSync(actualToolCacheDir, defaultToolCacheDir, 'junction'); 79 | 80 | // .complete file is required for github actions to make the cacheDir persistent 81 | const actualToolCacheCompleteFile = `${actualToolCacheDir}.complete`; 82 | const defaultToolCacheCompleteFile = `${defaultToolCacheDir}.complete`; 83 | await io.rmRF(defaultToolCacheCompleteFile); 84 | fs.symlinkSync(actualToolCacheCompleteFile, defaultToolCacheCompleteFile, 'file'); 85 | 86 | return actualToolCacheDir; 87 | } finally { 88 | process.env['RUNNER_TOOL_CACHE'] = defaultToolCacheRoot; 89 | } 90 | } 91 | 92 | async function makeDefaultToolpath(release: Release): Promise { 93 | fs.writeFileSync(".keep", ""); 94 | let toolpath = await tc.cacheFile(".keep", ".keep", "MATLAB", release.version); 95 | await io.rmRF(".keep"); 96 | return toolpath 97 | } 98 | 99 | export async function setupBatch(platform: string, architecture: string) { 100 | if (architecture != "x64" && !(platform == "darwin" && architecture == "arm64")) { 101 | return Promise.reject(Error(`This action is not supported on ${platform} runners using the ${architecture} architecture.`)); 102 | } 103 | 104 | let matlabBatchUrl: string; 105 | let matlabBatchExt: string = ""; 106 | switch (platform) { 107 | case "win32": 108 | matlabBatchExt = ".exe"; 109 | matlabBatchUrl = properties.matlabBatchRootUrl + "win64/matlab-batch.exe"; 110 | break; 111 | case "linux": 112 | matlabBatchUrl = properties.matlabBatchRootUrl + "glnxa64/matlab-batch"; 113 | break; 114 | case "darwin": 115 | if (architecture == "x64") { 116 | matlabBatchUrl = properties.matlabBatchRootUrl + "maci64/matlab-batch"; 117 | } else { 118 | matlabBatchUrl = properties.matlabBatchRootUrl + "maca64/matlab-batch"; 119 | } 120 | break; 121 | default: 122 | return Promise.reject(Error(`This action is not supported on ${platform} runners.`)); 123 | } 124 | 125 | let matlabBatch: string = await tc.downloadTool(matlabBatchUrl); 126 | let cachedPath = await tc.cacheFile(matlabBatch, `matlab-batch${matlabBatchExt}`, "matlab-batch", "v1"); 127 | core.addPath(cachedPath); 128 | if (platform !== "win32") { 129 | const exitCode = await exec.exec(`chmod +x ${path.join(cachedPath, 'matlab-batch'+matlabBatchExt)}`); 130 | if (exitCode !== 0) { 131 | return Promise.reject(Error("Unable to make matlab-batch executable.")); 132 | } 133 | } 134 | return 135 | } 136 | 137 | export async function getReleaseInfo(release: string): Promise { 138 | // Get release name from input parameter 139 | let name: string; 140 | let isPrerelease: boolean = false; 141 | const trimmedRelease = release.toLowerCase().trim() 142 | if (trimmedRelease === "latest" || trimmedRelease === "latest-including-prerelease") { 143 | try { 144 | const client: http.HttpClient = new http.HttpClient(undefined, [], { allowRetries: true, maxRetries: 3 }); 145 | const latestResp = await client.get(`${properties.matlabReleaseInfoUrl}${trimmedRelease}`); 146 | name = await latestResp.readBody(); 147 | } 148 | catch { 149 | return Promise.reject(Error(`Unable to retrieve the MATLAB release information. Contact MathWorks at continuous-integration@mathworks.com if the problem persists.`)); 150 | } 151 | } else { 152 | const nameMatch = trimmedRelease.match(/r[0-9]{4}[a-b]/); 153 | if (!nameMatch) { 154 | return Promise.reject(Error(`${release} is invalid or unsupported. Specify the value as R2020a or a later release.`)); 155 | } 156 | name = nameMatch[0]; 157 | } 158 | 159 | // create semantic version of format year.semiannual.update from release 160 | const year = name.slice(1,5); 161 | const semiannual = name[5] === "a"? "1": "2"; 162 | const updateMatch = release.toLowerCase().match(/u[0-9]+/); 163 | let version = `${year}.${semiannual}`; 164 | let update: string; 165 | if (updateMatch) { 166 | update = updateMatch[0] 167 | version += `.${update[1]}`; 168 | } else { 169 | // Notify user if Update version format is invalid 170 | if (trimmedRelease !== name && trimmedRelease !== "latest" && trimmedRelease !== "latest-including-prerelease") { 171 | const invalidUpdate = trimmedRelease.replace(name, ""); 172 | return Promise.reject(Error(`${invalidUpdate} is not a valid update release name.`)); 173 | } 174 | update = ""; 175 | version += ".999" 176 | if (name.includes("prerelease")) { 177 | name = name.replace("prerelease", "") 178 | version += "-prerelease"; 179 | isPrerelease = true; 180 | } 181 | } 182 | 183 | return { 184 | name: name, 185 | version: version, 186 | update: update, 187 | isPrerelease: isPrerelease, 188 | } 189 | } 190 | 191 | export function getSupportPackagesPath(platform: string, release: string): string | undefined { 192 | let supportPackagesDir; 193 | let capitalizedRelease = release[0].toUpperCase() + release.slice(1, release.length); 194 | switch (platform) { 195 | case "win32": 196 | supportPackagesDir = path.join("C:", "ProgramData", "MATLAB", "SupportPackages", capitalizedRelease); 197 | break; 198 | case "linux": 199 | case "darwin": 200 | supportPackagesDir = path.join(homedir(), "Documents", "MATLAB", "SupportPackages", capitalizedRelease); 201 | break; 202 | default: 203 | throw(`This action is not supported on ${platform} runners.`); 204 | } 205 | return supportPackagesDir; 206 | } 207 | 208 | export async function installSystemDependencies(platform: string, architecture: string, release: string) { 209 | if (platform === "linux") { 210 | return script.downloadAndRunScript(platform, properties.matlabDepsUrl, [release]); 211 | } else if (platform === "darwin" && architecture === "arm64") { 212 | if (release < "r2023b") { 213 | return installAppleSiliconRosetta(); 214 | } else { 215 | return installAppleSiliconJdk(); 216 | } 217 | } 218 | } 219 | 220 | async function installAppleSiliconRosetta() { 221 | const exitCode = await exec.exec(`sudo softwareupdate --install-rosetta --agree-to-license`); 222 | if (exitCode !== 0) { 223 | return Promise.reject(Error("Unable to install Rosetta 2.")); 224 | } 225 | } 226 | 227 | async function installAppleSiliconJdk() { 228 | const jdkPath = path.join(process.env["RUNNER_TEMP"] ?? "", "jdk.pkg"); 229 | await io.rmRF(jdkPath); 230 | const jdk = await tc.downloadTool(properties.appleSiliconJdkUrl, jdkPath); 231 | const exitCode = await exec.exec(`sudo installer -pkg "${jdk}" -target /`); 232 | if (exitCode !== 0) { 233 | return Promise.reject(Error("Unable to install Java runtime.")); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Action for Setting Up MATLAB 2 | 3 | The [Setup MATLAB](#set-up-matlab) action enables you to set up MATLAB® and other MathWorks® products on a [GitHub®-hosted](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners) (Linux®, Windows®, or macOS) runner or [self-hosted](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners) UNIX® (Linux or macOS) runner. When you specify this action as part of your workflow, the action sets up your preferred MATLAB release (R2021a or later) on the runner. If you do not specify a release, the action sets up the latest release of MATLAB. As part of the setup process, the action prepends MATLAB to the `PATH` system environment variable. 4 | 5 | >**Note:** For GitHub-hosted runners, the **Setup MATLAB** action automatically includes the dependencies required to run MATLAB and other MathWorks products. However, if you are using a self-hosted runner, you must ensure that the required dependencies are available on your runner. For details, see [Required Software on Self-Hosted Runners](#required-software-on-self-hosted-runners). 6 | 7 | ## Examples 8 | Once you set up MATLAB on a runner, you can build and test your MATLAB project as part of your workflow. To execute code on the runner, include the [Run MATLAB Build](https://github.com/matlab-actions/run-build/), [Run MATLAB Tests](https://github.com/matlab-actions/run-tests/), or [Run MATLAB Command](https://github.com/matlab-actions/run-command/) action in your workflow. 9 | 10 | ### Run Default Tasks in Build File 11 | Using the latest release of MATLAB on a GitHub-hosted runner, run the default tasks in a build file named `buildfile.m` in the root of your repository as well as all the tasks on which they depend. To set up the latest release of MATLAB on the runner, specify the **Setup MATLAB** action in your workflow. To run the tasks, specify the [Run MATLAB Build](https://github.com/matlab-actions/run-build/) action. 12 | 13 | ```yaml 14 | name: Run Default Tasks in Build File 15 | on: [push] 16 | jobs: 17 | my-job: 18 | name: Run MATLAB Build 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Check out repository 22 | uses: actions/checkout@v4 23 | - name: Set up MATLAB 24 | uses: matlab-actions/setup-matlab@v2 25 | - name: Run build 26 | uses: matlab-actions/run-build@v2 27 | ``` 28 | 29 | ### Run Tests in Parallel 30 | Run your MATLAB and Simulink® tests in parallel (requires Parallel Computing Toolbox™) using the latest release of the required products on a GitHub-hosted runner. To set up the latest release of MATLAB, Simulink, Simulink Test, and Parallel Computing Toolbox on the runner, specify the **Setup MATLAB** action with its `products` input in your workflow. To run the tests in parallel, specify the [Run MATLAB Tests](https://github.com/matlab-actions/run-tests/) action with its `use-parallel` input specified as `true`. 31 | 32 | ```YAML 33 | name: Run Tests in Parallel 34 | on: [push] 35 | jobs: 36 | my-job: 37 | name: Run MATLAB and Simulink Tests 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Check out repository 41 | uses: actions/checkout@v4 42 | - name: Set up products 43 | uses: matlab-actions/setup-matlab@v2 44 | with: 45 | products: > 46 | Simulink 47 | Simulink_Test 48 | Parallel_Computing_Toolbox 49 | - name: Run tests 50 | uses: matlab-actions/run-tests@v2 51 | with: 52 | use-parallel: true 53 | ``` 54 | 55 | ### Run MATLAB Script 56 | Using the latest release of MATLAB on a GitHub-hosted runner, run a script named `myscript.m` in the root of your repository. To set up the latest release of MATLAB on the runner, specify the **Setup MATLAB** action in your workflow. To run the script, specify the [Run MATLAB Command](https://github.com/matlab-actions/run-command/) action. 57 | 58 | ```yaml 59 | name: Run MATLAB Script 60 | on: [push] 61 | jobs: 62 | my-job: 63 | name: Run MATLAB Script 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Check out repository 67 | uses: actions/checkout@v4 68 | - name: Set up MATLAB 69 | uses: matlab-actions/setup-matlab@v2 70 | - name: Run script 71 | uses: matlab-actions/run-command@v2 72 | with: 73 | command: myscript 74 | ``` 75 | 76 | ### Use MATLAB Batch Licensing Token 77 | When you define a workflow using the **Setup MATLAB** action, you need a [MATLAB batch licensing token](https://github.com/mathworks-ref-arch/matlab-dockerfile/blob/main/alternates/non-interactive/MATLAB-BATCH.md#matlab-batch-licensing-token) if your project is private or if your workflow uses transformation products, such as MATLAB Coder™ and MATLAB Compiler™. Batch licensing tokens are strings that enable MATLAB to start in noninteractive environments. You can request a token by submitting the [MATLAB Batch Licensing Pilot](https://www.mathworks.com/support/batch-tokens.html) form. 78 | 79 | To use a MATLAB batch licensing token: 80 | 81 | 1. Set the token as a secret. For more information about secrets, see [Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions). 82 | 2. Map the secret to an environment variable named `MLM_LICENSE_TOKEN` in your workflow. 83 | 84 | For example, define a workflow that runs the tests in your private project by using the latest release of MATLAB on a self-hosted UNIX runner: 85 | - To set up the latest release of MATLAB on the self-hosted UNIX runner, specify the **Setup MATLAB** action in your workflow. (The runner must include all the dependencies required to run MATLAB.) 86 | - To run the tests, specify the [Run MATLAB Tests](https://github.com/matlab-actions/run-tests/) action. License MATLAB to run the tests by mapping a secret to the `MLM_LICENSE_TOKEN` environment variable in your workflow. In this example, `MyToken` is the name of the secret that holds the batch licensing token. 87 | 88 | ```YAML 89 | name: Use MATLAB Batch Licensing Token 90 | on: [push] 91 | env: 92 | MLM_LICENSE_TOKEN: ${{ secrets.MyToken }} 93 | jobs: 94 | my-job: 95 | name: Run MATLAB Tests in Private Project 96 | runs-on: self-hosted 97 | steps: 98 | - name: Check out repository 99 | uses: actions/checkout@v4 100 | - name: Set up MATLAB 101 | uses: matlab-actions/setup-matlab@v2 102 | - name: Run tests 103 | uses: matlab-actions/run-tests@v2 104 | ``` 105 | 106 | ### Build Across Multiple Platforms 107 | The **Setup MATLAB** action supports the Linux, Windows, and macOS platforms. Define a matrix of job configurations to run a build using the MATLAB build tool on all the supported platforms. This workflow runs three jobs, one for each value in the variable `os`. For more information about matrices, see [Using a matrix for your jobs](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs). 108 | 109 | ```YAML 110 | name: Build Across Multiple Platforms 111 | on: [push] 112 | jobs: 113 | my-job: 114 | name: Run MATLAB Build 115 | strategy: 116 | matrix: 117 | os: [ubuntu-latest, windows-latest, macos-latest] 118 | runs-on: ${{ matrix.os }} 119 | steps: 120 | - name: Check out repository 121 | uses: actions/checkout@v4 122 | - name: Set up MATLAB 123 | uses: matlab-actions/setup-matlab@v2 124 | - name: Run build 125 | uses: matlab-actions/run-build@v2 126 | with: 127 | tasks: test 128 | ``` 129 | 130 | ## Set Up MATLAB 131 | When you define your workflow in the `.github/workflows` directory of your repository, specify the **Setup MATLAB** action as `matlab-actions/setup-matlab@v2`. The action accepts optional inputs. 132 | 133 | | Input | Description | 134 | |-----------|-------------| 135 | | `release` |

(Optional) MATLAB release to set up. You can specify R2021a or a later release. By default, the value of `release` is `latest`, which corresponds to the latest release of MATLAB.

  • To set up the latest update of a release, specify only the release name, for example, `R2024a`.
  • To set up a specific update release, specify the release name with an update number suffix, for example, `R2024aU4`.
  • To set up a release without updates, specify the release name with an update 0 or general release suffix, for example, `R2024aU0` or `R2024aGR`.

**Example**: `release: R2024a`
**Example**: `release: latest`
**Example**: `release: R2024aU4`

136 | | `products` |

(Optional) Products to set up in addition to MATLAB, specified as a list of product names separated by spaces. You can specify `products` to set up most MathWorks products and support packages. The action uses [MATLAB Package Manager](https://github.com/mathworks-ref-arch/matlab-dockerfile/blob/main/MPM.md) (`mpm`) to set up products.

For a list of supported products, open the input file for your preferred release from the [`mpm-input-files`](https://github.com/mathworks-ref-arch/matlab-dockerfile/tree/main/mpm-input-files) folder on GitHub. Specify products using the format shown in the input file, excluding the `#product.` prefix. For example, to set up Deep Learning Toolbox™ in addition to MATLAB, specify `products: Deep_Learning_Toolbox`.

For an example of how to use the `products` input, see [Run Tests in Parallel](#run-tests-in-parallel).

**Example**: `products: Simulink`
**Example:** `products: Simulink Deep_Learning_Toolbox`

137 | | `cache` |

(Optional) Option to enable caching with GitHub Actions, specified as `false` or `true`. By default, the value is `false` and the action does not store MATLAB and the specified products in a GitHub Actions cache for future use. For more information about caching with GitHub Actions, see [Caching dependencies to speed up workflows](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows).

**Example**: `cache: true`

138 | 139 | #### Required Software on Self-Hosted Runners 140 | Before using the **Setup MATLAB** action to set up MATLAB and other MathWorks products on a self-hosted UNIX runner, verify that the required software is installed on your runner. 141 | 142 | ##### Linux 143 | If you are using a Linux runner, verify that the following software is installed on your runner: 144 | - Third-party packages required to run the `mpm` command — To view the list of `mpm` dependencies, refer to the Linux section of [Get MATLAB Package Manager](https://www.mathworks.com/help/install/ug/get-mpm-os-command-line.html). 145 | - All MATLAB dependencies — To view the list of MATLAB dependencies, go to the [MATLAB Dependencies](https://github.com/mathworks-ref-arch/container-images/tree/main/matlab-deps) repository on GitHub. Then, open the `//base-dependencies.txt` file for your MATLAB release and your runner's operating system. 146 | 147 | ##### macOS 148 | If you are using a macOS runner with an Apple silicon processor, verify that Java® Runtime Environment (JRE™) is installed on your runner. For information about this requirement and to get a compatible JRE version, see [MATLAB on Apple Silicon Macs](https://www.mathworks.com/support/requirements/apple-silicon.html). 149 | 150 | >**Tip:** One convenient way to include the required dependencies on a self-hosted runner is to specify the [MATLAB Dependencies container image on Docker® Hub](https://hub.docker.com/r/mathworks/matlab-deps) in your workflow. 151 | 152 | #### Licensing 153 | Product licensing for your workflow depends on your project visibility: 154 | 155 | - Public project — The [Run MATLAB Build](https://github.com/matlab-actions/run-build/), [Run MATLAB Tests](https://github.com/matlab-actions/run-tests/), and [Run MATLAB Command](https://github.com/matlab-actions/run-command/) actions automatically license all products for you, except for transformation products, such as MATLAB Coder and MATLAB Compiler. 156 | - Private project — The actions do not automatically license any products for you. 157 | 158 | To license products that are not automatically licensed, you can request a [MATLAB batch licensing token](https://github.com/mathworks-ref-arch/matlab-dockerfile/blob/main/alternates/non-interactive/MATLAB-BATCH.md#matlab-batch-licensing-token) by submitting the [MATLAB Batch Licensing Pilot](https://www.mathworks.com/support/batch-tokens.html) form. Batch licensing tokens are strings that enable MATLAB to start in noninteractive environments. 159 | 160 | To use a MATLAB batch licensing token, first set it as a [secret](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions). Then, map the secret to an environment variable named `MLM_LICENSE_TOKEN` in your workflow. For an example, see [Use MATLAB Batch Licensing Token](#use-matlab-batch-licensing-token). 161 | 162 | ## Notes 163 | - The **Setup MATLAB** action automatically includes the [MATLAB batch licensing executable](https://github.com/mathworks-ref-arch/matlab-dockerfile/blob/main/alternates/non-interactive/MATLAB-BATCH.md) (`matlab-batch`). To use a MATLAB batch licensing token in a workflow that does not use this action, you must first download the executable and add it to the system path. 164 | - Public project and MATLAB batch licensing do not support external language interfaces, including MATLAB Engine APIs for Python®, Java, .NET, COM, C, C++, and Fortran. To use external language interfaces in your workflow, use a self-hosted runner that has a version of MATLAB licensed without a batch token. 165 | - When you use the **Setup MATLAB** action, you execute third-party code that is licensed under separate terms. 166 | 167 | ## See Also 168 | - [Action for Running MATLAB Builds](https://github.com/matlab-actions/run-build/) 169 | - [Action for Running MATLAB Tests](https://github.com/matlab-actions/run-tests/) 170 | - [Action for Running MATLAB Commands](https://github.com/matlab-actions/run-command/) 171 | - [Continuous Integration with MATLAB and Simulink](https://www.mathworks.com/solutions/continuous-integration.html) 172 | 173 | ## Feedback and Support 174 | If you encounter a product licensing issue, consider requesting a MATLAB batch licensing token to use in your workflow. For more information, see [Use MATLAB Batch Licensing Token](#use-matlab-batch-licensing-token). 175 | 176 | If you have an enhancement request or other feedback about this action, create an issue on the [Issues](https://github.com/matlab-actions/setup-matlab/issues) page. 177 | 178 | For support, contact [MathWorks Technical Support](https://www.mathworks.com/support/contact_us.html). 179 | -------------------------------------------------------------------------------- /src/matlab.unit.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The MathWorks, Inc. 2 | 3 | import * as core from "@actions/core"; 4 | import * as exec from "@actions/exec"; 5 | import * as http from "@actions/http-client"; 6 | import * as httpjs from "http"; 7 | import * as net from "net"; 8 | import * as path from "path"; 9 | import * as tc from "@actions/tool-cache"; 10 | import * as matlab from "./matlab"; 11 | import * as script from "./script"; 12 | import fs from "fs"; 13 | import properties from "./properties.json"; 14 | 15 | jest.mock("http"); 16 | jest.mock("net"); 17 | jest.mock("@actions/core"); 18 | jest.mock("@actions/exec"); 19 | jest.mock("@actions/http-client"); 20 | jest.mock("@actions/tool-cache"); 21 | jest.mock("./script"); 22 | 23 | afterEach(() => { 24 | jest.resetAllMocks(); 25 | }); 26 | 27 | describe("matlab tests", () => { 28 | const release = { 29 | name: "r2022b", 30 | version: "2022.2.999", 31 | update: "", 32 | isPrerelease: false, 33 | } 34 | const platform = "linux"; 35 | 36 | describe("toolcacheLocation", () => { 37 | let findMock: jest.Mock; 38 | let cacheFileMock: jest.Mock; 39 | let infoMock: jest.Mock; 40 | 41 | beforeEach(() => { 42 | findMock = tc.find as jest.Mock; 43 | cacheFileMock = tc.cacheFile as jest.Mock; 44 | infoMock = core.info as jest.Mock; 45 | }); 46 | 47 | it("returns toolpath if in toolcache", async () => { 48 | findMock.mockReturnValue("/opt/hostedtoolcache/matlab/r2022b"); 49 | await expect(matlab.getToolcacheDir(platform, release)).resolves.toMatchObject(["/opt/hostedtoolcache/matlab/r2022b", true]); 50 | expect(infoMock).toHaveBeenCalledTimes(1); 51 | }); 52 | 53 | it("creates cache and returns default path for linux", async () => { 54 | findMock.mockReturnValue(""); 55 | cacheFileMock.mockReturnValue("/opt/hostedtoolcache/matlab/r2022b"); 56 | await expect(matlab.getToolcacheDir(platform, release)).resolves.toMatchObject(["/opt/hostedtoolcache/matlab/r2022b", false]); 57 | }); 58 | 59 | it("creates cache and returns default path for mac", async () => { 60 | findMock.mockReturnValue(""); 61 | cacheFileMock.mockReturnValue("/opt/hostedtoolcache/matlab/r2022b"); 62 | await expect(matlab.getToolcacheDir("darwin", release)).resolves.toMatchObject(["/opt/hostedtoolcache/matlab/r2022b/MATLAB.app", false]); 63 | }); 64 | 65 | describe("windows performance workaround", () => { 66 | let runnerEnv: string | undefined; 67 | let agentIsSelfHosted: string | undefined; 68 | let runnerToolcache: string | undefined; 69 | 70 | afterEach(() => { 71 | process.env["RUNNER_ENVIRONMENT"] = runnerEnv; 72 | process.env["AGENT_ISSELFHOSTED"] = agentIsSelfHosted; 73 | process.env["RUNNER_TOOL_CACHE"] = runnerToolcache; 74 | }); 75 | 76 | beforeEach(() => { 77 | runnerEnv = process.env["RUNNER_ENVIRONMENT"]; 78 | agentIsSelfHosted = process.env["AGENT_ISSELFHOSTED"]; 79 | runnerToolcache = process.env["RUNNER_TOOL_CACHE"]; 80 | 81 | process.env["RUNNER_TOOL_CACHE"] = "C:\\hostedtoolcache\\windows\\matlab\\r2022b"; 82 | cacheFileMock.mockImplementation(() => process.env["RUNNER_TOOL_CACHE"]); 83 | findMock.mockReturnValue(""); 84 | }); 85 | 86 | it("uses workaround if github-hosted", async () => { 87 | let expectedToolcacheDir = "D:\\hostedtoolcache\\windows\\matlab\\r2022b"; 88 | 89 | // replicate github-hosted environment 90 | process.env["AGENT_ISSELFHOSTED"] = "0"; 91 | process.env["RUNNER_ENVIRONMENT"] = "github-hosted"; 92 | // mock & no-op fs operations 93 | let existsSyncSpy = jest.spyOn(fs, "existsSync").mockReturnValue(true); 94 | let mkdirSyncSpy = jest.spyOn(fs, "mkdirSync").mockImplementation(() => ""); 95 | let symlinkSyncSpy = jest.spyOn(fs, "symlinkSync").mockImplementation(() => {}); 96 | 97 | await expect(matlab.getToolcacheDir("win32", release)).resolves.toMatchObject([expectedToolcacheDir, false]); 98 | expect(existsSyncSpy).toHaveBeenCalledTimes(2); 99 | expect(mkdirSyncSpy).toHaveBeenCalledTimes(1); 100 | expect(symlinkSyncSpy).toHaveBeenCalledTimes(2); 101 | }); 102 | 103 | it("uses default toolcache directory if not github hosted", async () => { 104 | let expectedToolcacheDir = "C:\\hostedtoolcache\\windows\\matlab\\r2022b"; 105 | process.env["AGENT_ISSELFHOSTED"] = "1"; 106 | process.env["RUNNER_ENVIRONMENT"] = "self-hosted"; 107 | await expect(matlab.getToolcacheDir("win32", release)).resolves.toMatchObject([expectedToolcacheDir, false]); 108 | }); 109 | 110 | it("uses default toolcache directory toolcache directory is not defined", async () => { 111 | let expectedToolcacheDir = "C:\\hostedtoolcache\\windows\\matlab\\r2022b"; 112 | process.env["RUNNER_TOOL_CACHE"] = ''; 113 | cacheFileMock.mockReturnValue(expectedToolcacheDir); 114 | await expect(matlab.getToolcacheDir("win32", release)).resolves.toMatchObject([expectedToolcacheDir, false]); 115 | }); 116 | 117 | it("uses default toolcache directory if d: drive doesn't exist", async () => { 118 | jest.spyOn(fs, "existsSync").mockReturnValue(false); 119 | let expectedToolcacheDir = "C:\\hostedtoolcache\\windows\\matlab\\r2022b"; 120 | await expect(matlab.getToolcacheDir("win32", release)).resolves.toMatchObject([expectedToolcacheDir, false]); 121 | }); 122 | 123 | it("uses default toolcache directory if c: drive doesn't exist", async () => { 124 | jest.spyOn(fs, "existsSync").mockReturnValueOnce(true).mockReturnValue(false); 125 | let expectedToolcacheDir = "C:\\hostedtoolcache\\windows\\matlab\\r2022b"; 126 | await expect(matlab.getToolcacheDir("win32", release)).resolves.toMatchObject([expectedToolcacheDir, false]); 127 | 128 | }); 129 | }); 130 | }); 131 | 132 | describe("setupBatch", () => { 133 | let tcDownloadToolMock: jest.Mock; 134 | let cacheFileMock: jest.Mock; 135 | let execMock: jest.Mock; 136 | const arch = "x64"; 137 | const batchMockPath = path.join("path", "to", "matlab-batch"); 138 | 139 | beforeEach(() => { 140 | tcDownloadToolMock = tc.downloadTool as jest.Mock; 141 | cacheFileMock = tc.cacheFile as jest.Mock; 142 | execMock = exec.exec as jest.Mock; 143 | process.env.RUNNER_TEMP = path.join("runner", "workdir", "tmp"); 144 | 145 | tcDownloadToolMock.mockResolvedValue(batchMockPath); 146 | cacheFileMock.mockResolvedValue(batchMockPath); 147 | execMock.mockResolvedValue(0); 148 | }); 149 | 150 | describe("test on all supported platforms", () => { 151 | it(`works on linux`, async () => { 152 | const platform = "linux"; 153 | await expect(matlab.setupBatch(platform, arch)).resolves.toBeUndefined(); 154 | expect(cacheFileMock).toHaveBeenCalledTimes(1); 155 | }); 156 | 157 | it(`works on windows`, async () => { 158 | const platform = "win32"; 159 | await expect(matlab.setupBatch(platform, arch)).resolves.toBeUndefined(); 160 | }); 161 | 162 | it(`works on mac`, async () => { 163 | const platform = "darwin"; 164 | await expect(matlab.setupBatch(platform, arch)).resolves.toBeUndefined(); 165 | }); 166 | 167 | it(`works on mac with apple silicon`, async () => { 168 | const platform = "darwin"; 169 | execMock.mockResolvedValue(0); 170 | await expect(matlab.setupBatch(platform, "arm64")).resolves.toBeUndefined(); 171 | }); 172 | }); 173 | 174 | it("errors on unsupported platform", async () => { 175 | await expect(() => matlab.setupBatch('sunos', arch)).rejects.toBeDefined(); 176 | }); 177 | 178 | it("errors on unsupported architecture", async () => { 179 | const platform = "linux"; 180 | await expect(() => matlab.setupBatch(platform, 'x86')).rejects.toBeDefined(); 181 | }); 182 | 183 | it("works without RUNNER_TEMP", async () => { 184 | const platform = "linux"; 185 | process.env.RUNNER_TEMP = ''; 186 | await expect(matlab.setupBatch(platform, arch)).resolves.toBeUndefined(); 187 | }); 188 | 189 | it("rejects when the download fails", async () => { 190 | const platform = "linux"; 191 | tcDownloadToolMock.mockRejectedValue(Error("oof")); 192 | await expect(matlab.setupBatch(platform, arch)).rejects.toBeDefined(); 193 | }); 194 | 195 | it("rejects when the chmod fails", async () => { 196 | const platform = "linux"; 197 | execMock.mockResolvedValue(1); 198 | await expect(matlab.setupBatch(platform, arch)).rejects.toBeDefined(); 199 | }); 200 | }); 201 | 202 | describe("getReleaseInfo", () => { 203 | beforeEach(() => { 204 | jest.spyOn(http.HttpClient.prototype, 'get').mockImplementation(async () => { 205 | return { 206 | message: new httpjs.IncomingMessage(new net.Socket()), 207 | readBody: () => {return Promise.resolve("r2022b")} 208 | }; 209 | }) 210 | }); 211 | 212 | it("latest-including-prereleases resolves", () => { 213 | expect(matlab.getReleaseInfo("latest")).resolves.toMatchObject(release); 214 | }); 215 | 216 | it("prerelease-latest resolves", () => { 217 | const prereleaseName = "r2022bprerelease" 218 | const prerelease = { 219 | name: "r2022b", 220 | version: "2022.2.999-prerelease", 221 | update: "", 222 | isPrerelease: true, 223 | } 224 | jest.spyOn(http.HttpClient.prototype, 'get').mockImplementation(async () => { 225 | return { 226 | message: new httpjs.IncomingMessage(new net.Socket()), 227 | readBody: () => {return Promise.resolve(prereleaseName)} 228 | }; 229 | }) 230 | expect(matlab.getReleaseInfo("latest-including-prerelease")).resolves.toMatchObject(prerelease); 231 | }); 232 | 233 | it("case insensitive", () => { 234 | expect(matlab.getReleaseInfo("R2022b")).resolves.toMatchObject(release); 235 | }); 236 | 237 | it("Sets minor version according to a or b release", () => { 238 | const R2022aRelease = { 239 | name: "r2022a", 240 | update: "", 241 | version: "2022.1.999", 242 | isPrerelease: false, 243 | } 244 | expect(matlab.getReleaseInfo("R2022a")).resolves.toMatchObject(R2022aRelease); 245 | 246 | const R2022bRelease = { 247 | name: "r2022b", 248 | update: "", 249 | version: "2022.2.999", 250 | isPrerelease: false, 251 | } 252 | expect(matlab.getReleaseInfo("R2022b")).resolves.toMatchObject(R2022bRelease); 253 | }); 254 | 255 | it("allows specifying update number", () => { 256 | const releaseWithUpdate = { 257 | name: "r2022b", 258 | update: "u2", 259 | version: "2022.2.2", 260 | } 261 | expect(matlab.getReleaseInfo("R2022bU2")).resolves.toMatchObject(releaseWithUpdate); 262 | }); 263 | 264 | it("displays message for invalid update level input format and uses latest", () => { 265 | expect(matlab.getReleaseInfo("r2022bUpdate1")).rejects.toBeDefined(); 266 | }); 267 | 268 | it("rejects for unsupported release", () => { 269 | expect(matlab.getReleaseInfo("R2022c")).rejects.toBeDefined(); 270 | }); 271 | 272 | it("rejects if for bad http response", () => { 273 | jest.spyOn(http.HttpClient.prototype, 'get').mockImplementation(async () => { 274 | return { 275 | message: new httpjs.IncomingMessage(new net.Socket()), 276 | readBody: () => {return Promise.reject("Bam!")} 277 | }; 278 | }) 279 | expect(matlab.getReleaseInfo("latest")).rejects.toBeDefined(); 280 | }); 281 | }); 282 | 283 | describe("installSystemDependencies", () => { 284 | let downloadAndRunScriptMock: jest.Mock; 285 | let tcDownloadToolMock: jest.Mock; 286 | let execMock: jest.Mock; 287 | const arch = "x64"; 288 | const release = "r2023b"; 289 | 290 | beforeEach(() => { 291 | downloadAndRunScriptMock = script.downloadAndRunScript as jest.Mock; 292 | tcDownloadToolMock = tc.downloadTool as jest.Mock; 293 | execMock = exec.exec as jest.Mock; 294 | }); 295 | 296 | describe("test on all supported platforms", () => { 297 | it(`works on linux`, async () => { 298 | const platform = "linux"; 299 | await expect( 300 | matlab.installSystemDependencies(platform, arch, release) 301 | ).resolves.toBeUndefined(); 302 | expect(downloadAndRunScriptMock).toHaveBeenCalledWith( 303 | platform, 304 | properties.matlabDepsUrl, 305 | [release] 306 | ); 307 | }); 308 | 309 | it(`works on windows`, async () => { 310 | const platform = "win32"; 311 | await expect( 312 | matlab.installSystemDependencies(platform, arch, release) 313 | ).resolves.toBeUndefined(); 314 | }); 315 | 316 | it(`works on mac`, async () => { 317 | const platform = "darwin"; 318 | await expect( 319 | matlab.installSystemDependencies(platform, arch, release) 320 | ).resolves.toBeUndefined(); 321 | }); 322 | 323 | it(`works on mac with apple silicon`, async () => { 324 | const platform = "darwin"; 325 | tcDownloadToolMock.mockResolvedValue("java.jdk"); 326 | execMock.mockResolvedValue(0); 327 | await expect( 328 | matlab.installSystemDependencies(platform, "arm64", release) 329 | ).resolves.toBeUndefined(); 330 | expect(tcDownloadToolMock).toHaveBeenCalledWith(properties.appleSiliconJdkUrl, expect.anything()); 331 | expect(execMock).toHaveBeenCalledWith(`sudo installer -pkg "java.jdk" -target /`); 332 | }); 333 | 334 | it(`works on mac with apple silicon { 335 | const platform = "darwin"; 336 | execMock.mockResolvedValue(0); 337 | await expect( 338 | matlab.installSystemDependencies(platform, "arm64", "r2023a") 339 | ).resolves.toBeUndefined(); 340 | expect(execMock).toHaveBeenCalledWith(`sudo softwareupdate --install-rosetta --agree-to-license`); 341 | }); 342 | }); 343 | 344 | it("rejects when the apple silicon JDK download fails", async () => { 345 | const platform = "darwin"; 346 | tcDownloadToolMock.mockRejectedValue(Error("oof")); 347 | await expect( 348 | matlab.installSystemDependencies(platform, "arm64", release) 349 | ).rejects.toBeDefined(); 350 | }); 351 | 352 | it("rejects when the apple silicon JDK fails to install", async () => { 353 | const platform = "darwin"; 354 | tcDownloadToolMock.mockResolvedValue("java.jdk"); 355 | execMock.mockResolvedValue(1); 356 | await expect( 357 | matlab.installSystemDependencies(platform, "arm64", release) 358 | ).rejects.toBeDefined(); 359 | }); 360 | }); 361 | }); 362 | --------------------------------------------------------------------------------