Skip to content

Add a source argument to setup-matlab #149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ inputs:
MATLAB release to set up (R2021a or later)
required: false
default: latest
source:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sameagen-MW: Is this going to be an internal feature or are we going to document the input right away?

(https://www.mathworks.com/help/install/ug/mpminstall.html#mw_5abeb5ba-92a4-4ecf-8e4d-263760988c7e)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the goal is to document it right away. It has some niche use cases with self-hosted runners, but also acts as a mitigating factor if there's ever any future outages.

description: >-
Path to mounted ISO image or directory set up with `mpm download`
required: false
default: ""
products:
description: >-
Products to set up in addition to MATLAB, specified as a list of product names separated by spaces
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ export async function run() {
const platform = process.platform;
const architecture = process.arch;
const release = core.getInput("release");
const source = core.getInput("source");
const products = core.getMultilineInput("products");
const cache = core.getBooleanInput("cache");

if (source !== "") {
return install.installFromSource(platform, architecture, source, products);
}
return install.install(platform, architecture, release, products, cache);
}

Expand Down
39 changes: 38 additions & 1 deletion src/install.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2020-2025 The MathWorks, Inc.

import * as core from "@actions/core";
import * as crypto from "crypto";
import * as matlab from "./matlab";
import * as mpm from "./mpm";
import * as path from "path";
Expand Down Expand Up @@ -56,7 +57,7 @@ export async function install(platform: string, architecture: string, release: s
core.setOutput('matlabroot', destination);

await matlab.setupBatch(platform, matlabArch);

if (platform === "win32") {
if (matlabArch === "x86") {
core.addPath(path.join(destination, "runtime", "win32"));
Expand All @@ -68,3 +69,39 @@ export async function install(platform: string, architecture: string, release: s

return;
}

// Limitations
//
// * No system dependencies are installed
// * Does not cache
export async function installFromSource(platform: string, architecture: string, source: string, products: string[]) {
// Create release key
const releaseKey = 'source-' + crypto.createHash('sha256').update(source).digest('hex');
const releaseInfo = {
name: releaseKey,
version: "1.0.0",
update: "",
isPrerelease: false
};
Comment on lines +80 to +85
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about looking for this info in ProductFilesInfo.xml (should exist when using an mpm download folder) or VersionInfo.xml (should exist when using a mounted ISO folder)? Then we could install system dependencies and place MATLAB in the right tool cache location. If we can't find either file (which would probably indicate a corrupt or incorrect source folder anyway), this approach could be a fallback. We do something similar in run-matlab-command.


const [destination, alreadyExists]: [string, boolean] = await matlab.getToolcacheDir(platform, releaseInfo);

if (!alreadyExists) {
const mpmPath: string = await mpm.setup(platform, architecture);
await mpm.installFromSource(mpmPath, source, products, destination);
core.saveState(State.InstallSuccessful, 'true');
}

core.addPath(path.join(destination, "bin"));
core.setOutput('matlabroot', destination);

await matlab.setupBatch(platform, architecture);

if (platform === "win32") {
if (architecture === "x86") {
core.addPath(path.join(destination, "runtime", "win32"));
} else {
core.addPath(path.join(destination, "runtime", "win64"));
}
}
}
47 changes: 45 additions & 2 deletions src/install.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe("install procedure", () => {
let matlabSetupBatchMock: jest.Mock;
let mpmSetupMock: jest.Mock;
let mpmInstallMock: jest.Mock;
let mpmInstallSourceMock: jest.Mock;
let saveStateMock: jest.Mock;
let addPathMock: jest.Mock;
let setOutputMock: jest.Mock;
Expand Down Expand Up @@ -51,6 +52,7 @@ describe("install procedure", () => {
matlabSetupBatchMock = matlab.setupBatch as jest.Mock;
mpmSetupMock = mpm.setup as jest.Mock;
mpmInstallMock = mpm.install as jest.Mock;
mpmInstallSourceMock = mpm.installFromSource as jest.Mock;
saveStateMock = core.saveState as jest.Mock;
addPathMock = core.addPath as jest.Mock;
setOutputMock = core.setOutput as jest.Mock;
Expand Down Expand Up @@ -92,7 +94,7 @@ describe("install procedure", () => {
matlabGetReleaseInfoMock.mockResolvedValue({
name: "r2020a",
version: "9.8.0",
updateNumber: "latest"
updateNumber: "latest"
});
await expect(install.install(platform, arch, "r2020a", products, useCache)).rejects.toBeDefined();
});
Expand Down Expand Up @@ -124,11 +126,22 @@ describe("install procedure", () => {
expect(saveStateMock).toHaveBeenCalledTimes(0);
});

it("rejects when mpm installing from source fails", async () => {
mpmInstallSourceMock.mockRejectedValue(Error("oof"));
await expect(install.installFromSource("linux", "x64", "bad/path", ["MATLAB"])).rejects.toBeDefined();
expect(saveStateMock).toHaveBeenCalledTimes(0);
});

it("rejects when the matlab-batch install fails", async () => {
matlabSetupBatchMock.mockRejectedValueOnce(Error("oof"));
await expect(doInstall()).rejects.toBeDefined();
});

it("installing from source rejects when the matlab-batch install fails", async () => {
matlabSetupBatchMock.mockRejectedValueOnce(Error("oof"));
await expect(install.installFromSource("linux", "x64", "/path", ["MATLAB"])).rejects.toBeDefined();
});

it("Does not restore cache if useCache is false", async () => {
await expect(doInstall()).resolves.toBeUndefined();
expect(restoreMATLABMock).toHaveBeenCalledTimes(0);
Expand Down Expand Up @@ -156,7 +169,7 @@ describe("install procedure", () => {
matlabGetReleaseInfoMock.mockResolvedValue({
name: "r2023a",
version: "9.14.0",
updateNumber: "latest"
updateNumber: "latest"
});
await expect(install.install("darwin", "arm64", "r2023a", products, true)).resolves.toBeUndefined();
expect(matlabInstallSystemDependenciesMock).toHaveBeenCalledWith("darwin", "arm64", "r2023a");
Expand All @@ -171,4 +184,34 @@ describe("install procedure", () => {
expect(addPathMock).toHaveBeenCalledWith(expect.stringContaining("runtime"));
});

it("installs from source", async () => {
await expect(install.installFromSource("linux", "x64", "/dummy/path", ["MATLAB", "Parallel_Computing_Toolbox"]))
.resolves
.toBeUndefined();
expect(matlabInstallSystemDependenciesMock).toHaveBeenCalledTimes(0);
expect(matlabSetupBatchMock).toHaveBeenCalledTimes(1);
expect(mpmSetupMock).toHaveBeenCalledTimes(1);
expect(mpmInstallMock).toHaveBeenCalledTimes(0);
expect(mpmInstallSourceMock).toHaveBeenCalledTimes(1);
expect(saveStateMock).toHaveBeenCalledWith(State.InstallSuccessful, 'true');
expect(addPathMock).toHaveBeenCalledTimes(1);
expect(setOutputMock).toHaveBeenCalledTimes(1);
});

it("NoOp on existing install from source", async () => {
matlabGetToolcacheDirMock.mockResolvedValue(["/opt/hostedtoolcache/MATLAB/9.13.0/x64", true]);
await expect(install.installFromSource("linux", "x64", "/my/path", ["MATLAB"])).resolves.toBeUndefined();
expect(mpmInstallMock).toHaveBeenCalledTimes(0);
expect(saveStateMock).toHaveBeenCalledTimes(0);
expect(addPathMock).toHaveBeenCalledTimes(1);
expect(setOutputMock).toHaveBeenCalledTimes(1);
});

it("adds runtime path for Windows platform when installing from source", async () => {
await expect(install.installFromSource("win32", arch, "/dummy/path", products)).resolves.toBeUndefined();
expect(addPathMock).toHaveBeenCalledTimes(2);
expect(addPathMock).toHaveBeenCalledWith(expect.stringContaining("bin"));
expect(addPathMock).toHaveBeenCalledWith(expect.stringContaining("runtime"));
});

});
29 changes: 28 additions & 1 deletion src/mpm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export async function install(mpmPath: string, release: matlab.Release, products
"install",
`--release=${mpmRelease}`,
`--destination=${destination}`,
]
];
if (release.isPrerelease) {
mpmArguments = mpmArguments.concat(["--release-status=Prerelease"]);
}
Expand All @@ -89,3 +89,30 @@ export async function install(mpmPath: string, release: matlab.Release, products
}
return
}

export async function installFromSource(mpmPath: string, source: string, products: string[], destination: string) {
// remove spaces and flatten product list
let parsedProducts = products.flatMap(p => p.split(/[ ]+/));
// Add MATLAB by default
parsedProducts.push("MATLAB");
// Remove duplicate products
parsedProducts = [...new Set(parsedProducts)];

let mpmArguments: string[] = [
"install",
`--source=${source}`,
`--destination=${destination}`,
];
mpmArguments = mpmArguments.concat("--products", ...parsedProducts);

const exitCode = await exec.exec(mpmPath, mpmArguments).catch(async e => {
// Fully remove failed MATLAB installation for self-hosted runners
await rmRF(destination);
throw e;
});
if (exitCode !== 0) {
await rmRF(destination);
return Promise.reject(Error(`Script exited with non-zero code ${exitCode}`));
}
return
}
34 changes: 34 additions & 0 deletions src/mpm.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,38 @@ describe("mpm install", () => {
await expect(mpm.install(mpmPath, releaseInfo, products, destination)).rejects.toBeDefined();
expect(rmRFMock).toHaveBeenCalledWith(destination);
});

it("ideally works when installing from source", async () => {
const destination ="/opt/matlab";
const source = "/path/to/source";
const products = ["MATLAB", "Compiler"];
const expectedMpmArgs = [
"install",
`--source=${source}`,
`--destination=${destination}`,
"--products",
"MATLAB",
"Compiler",
]
execMock.mockResolvedValue(0);

await expect(mpm.installFromSource(mpmPath, source, products, destination)).resolves.toBeUndefined();
expect(execMock.mock.calls[0][1]).toMatchObject(expectedMpmArgs);
});

it("rejects and cleans on mpm rejection when installing from source", async () => {
const destination = "/opt/matlab";
const products = ["MATLAB", "Compiler"];
execMock.mockRejectedValue(1);
await expect(mpm.installFromSource(mpmPath, "/path", products, destination)).rejects.toBeDefined();
expect(rmRFMock).toHaveBeenCalledWith(destination);
});

it("rejects and cleans on failed install when installing from source", async () => {
const destination = "/opt/matlab";
const products = ["MATLAB", "Compiler"];
execMock.mockResolvedValue(1);
await expect(mpm.installFromSource(mpmPath, "/path", products, destination)).rejects.toBeDefined();
expect(rmRFMock).toHaveBeenCalledWith(destination);
});
});
Loading