Skip to content

Commit 2a699d4

Browse files
committed
Initial draft of adding source argument
1 parent 1dcf7e8 commit 2a699d4

File tree

6 files changed

+156
-4
lines changed

6 files changed

+156
-4
lines changed

action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ inputs:
99
MATLAB release to set up (R2021a or later)
1010
required: false
1111
default: latest
12+
source:
13+
description: >-
14+
Path to mounted ISO image or directory set up with `mpm download`
15+
required: false
16+
default: ""
1217
products:
1318
description: >-
1419
Products to set up in addition to MATLAB, specified as a list of product names separated by spaces

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ export async function run() {
1010
const platform = process.platform;
1111
const architecture = process.arch;
1212
const release = core.getInput("release");
13+
const source = core.getInput("source");
1314
const products = core.getMultilineInput("products");
1415
const cache = core.getBooleanInput("cache");
16+
17+
if (source !== "") {
18+
return install.installFromSource(platform, architecture, source, products);
19+
}
1520
return install.install(platform, architecture, release, products, cache);
1621
}
1722

src/install.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright 2020-2025 The MathWorks, Inc.
22

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

5859
await matlab.setupBatch(platform, matlabArch);
59-
60+
6061
if (platform === "win32") {
6162
if (matlabArch === "x86") {
6263
core.addPath(path.join(destination, "runtime", "win32"));
@@ -68,3 +69,40 @@ export async function install(platform: string, architecture: string, release: s
6869

6970
return;
7071
}
72+
73+
// Limitations
74+
//
75+
// * No system dependencies are installed
76+
// * Does not cache
77+
export async function installFromSource(platform: string, architecture: string, source: string, products: string[]) {
78+
// Create release key
79+
const name = path.basename(source);
80+
const releaseKey = 'source-' + crypto.createHash('sha256').update(source).digest('hex');
81+
const releaseInfo = {
82+
name: name,
83+
version: releaseKey,
84+
update: "",
85+
isPrerelease: false
86+
};
87+
88+
const [destination, alreadyExists]: [string, boolean] = await matlab.getToolcacheDir(platform, releaseInfo);
89+
90+
if (!alreadyExists) {
91+
const mpmPath: string = await mpm.setup(platform, architecture);
92+
await mpm.installFromSource(mpmPath, source, products, destination);
93+
core.saveState(State.InstallSuccessful, 'true');
94+
}
95+
96+
core.addPath(path.join(destination, "bin"));
97+
core.setOutput('matlabroot', destination);
98+
99+
await matlab.setupBatch(platform, architecture);
100+
101+
if (platform === "win32") {
102+
if (architecture === "x86") {
103+
core.addPath(path.join(destination, "runtime", "win32"));
104+
} else {
105+
core.addPath(path.join(destination, "runtime", "win64"));
106+
}
107+
}
108+
}

src/install.unit.test.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe("install procedure", () => {
2323
let matlabSetupBatchMock: jest.Mock;
2424
let mpmSetupMock: jest.Mock;
2525
let mpmInstallMock: jest.Mock;
26+
let mpmInstallSourceMock: jest.Mock;
2627
let saveStateMock: jest.Mock;
2728
let addPathMock: jest.Mock;
2829
let setOutputMock: jest.Mock;
@@ -51,6 +52,7 @@ describe("install procedure", () => {
5152
matlabSetupBatchMock = matlab.setupBatch as jest.Mock;
5253
mpmSetupMock = mpm.setup as jest.Mock;
5354
mpmInstallMock = mpm.install as jest.Mock;
55+
mpmInstallSourceMock = mpm.installFromSource as jest.Mock;
5456
saveStateMock = core.saveState as jest.Mock;
5557
addPathMock = core.addPath as jest.Mock;
5658
setOutputMock = core.setOutput as jest.Mock;
@@ -92,7 +94,7 @@ describe("install procedure", () => {
9294
matlabGetReleaseInfoMock.mockResolvedValue({
9395
name: "r2020a",
9496
version: "9.8.0",
95-
updateNumber: "latest"
97+
updateNumber: "latest"
9698
});
9799
await expect(install.install(platform, arch, "r2020a", products, useCache)).rejects.toBeDefined();
98100
});
@@ -124,11 +126,22 @@ describe("install procedure", () => {
124126
expect(saveStateMock).toHaveBeenCalledTimes(0);
125127
});
126128

129+
it("rejects when mpm installing from source fails", async () => {
130+
mpmInstallSourceMock.mockRejectedValue(Error("oof"));
131+
await expect(install.installFromSource("linux", "x64", "bad/path", ["MATLAB"])).rejects.toBeDefined();
132+
expect(saveStateMock).toHaveBeenCalledTimes(0);
133+
});
134+
127135
it("rejects when the matlab-batch install fails", async () => {
128136
matlabSetupBatchMock.mockRejectedValueOnce(Error("oof"));
129137
await expect(doInstall()).rejects.toBeDefined();
130138
});
131139

140+
it("installing from source rejects when the matlab-batch install fails", async () => {
141+
matlabSetupBatchMock.mockRejectedValueOnce(Error("oof"));
142+
await expect(install.installFromSource("linux", "x64", "/path", ["MATLAB"])).rejects.toBeDefined();
143+
});
144+
132145
it("Does not restore cache if useCache is false", async () => {
133146
await expect(doInstall()).resolves.toBeUndefined();
134147
expect(restoreMATLABMock).toHaveBeenCalledTimes(0);
@@ -156,7 +169,7 @@ describe("install procedure", () => {
156169
matlabGetReleaseInfoMock.mockResolvedValue({
157170
name: "r2023a",
158171
version: "9.14.0",
159-
updateNumber: "latest"
172+
updateNumber: "latest"
160173
});
161174
await expect(install.install("darwin", "arm64", "r2023a", products, true)).resolves.toBeUndefined();
162175
expect(matlabInstallSystemDependenciesMock).toHaveBeenCalledWith("darwin", "arm64", "r2023a");
@@ -171,4 +184,34 @@ describe("install procedure", () => {
171184
expect(addPathMock).toHaveBeenCalledWith(expect.stringContaining("runtime"));
172185
});
173186

187+
it("installs from source", async () => {
188+
await expect(install.installFromSource("linux", "x64", "/dummy/path", ["MATLAB", "Parallel_Computing_Toolbox"]))
189+
.resolves
190+
.toBeUndefined();
191+
expect(matlabInstallSystemDependenciesMock).toHaveBeenCalledTimes(0);
192+
expect(matlabSetupBatchMock).toHaveBeenCalledTimes(1);
193+
expect(mpmSetupMock).toHaveBeenCalledTimes(1);
194+
expect(mpmInstallMock).toHaveBeenCalledTimes(0);
195+
expect(mpmInstallSourceMock).toHaveBeenCalledTimes(1);
196+
expect(saveStateMock).toHaveBeenCalledWith(State.InstallSuccessful, 'true');
197+
expect(addPathMock).toHaveBeenCalledTimes(1);
198+
expect(setOutputMock).toHaveBeenCalledTimes(1);
199+
});
200+
201+
it("NoOp on existing install from source", async () => {
202+
matlabGetToolcacheDirMock.mockResolvedValue(["/opt/hostedtoolcache/MATLAB/9.13.0/x64", true]);
203+
await expect(install.installFromSource("linux", "x64", "/my/path", ["MATLAB"])).resolves.toBeUndefined();
204+
expect(mpmInstallMock).toHaveBeenCalledTimes(0);
205+
expect(saveStateMock).toHaveBeenCalledTimes(0);
206+
expect(addPathMock).toHaveBeenCalledTimes(1);
207+
expect(setOutputMock).toHaveBeenCalledTimes(1);
208+
});
209+
210+
it("adds runtime path for Windows platform when installing from source", async () => {
211+
await expect(install.installFromSource("win32", arch, "/dummy/path", products)).resolves.toBeUndefined();
212+
expect(addPathMock).toHaveBeenCalledTimes(2);
213+
expect(addPathMock).toHaveBeenCalledWith(expect.stringContaining("bin"));
214+
expect(addPathMock).toHaveBeenCalledWith(expect.stringContaining("runtime"));
215+
});
216+
174217
});

src/mpm.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export async function install(mpmPath: string, release: matlab.Release, products
7272
"install",
7373
`--release=${mpmRelease}`,
7474
`--destination=${destination}`,
75-
]
75+
];
7676
if (release.isPrerelease) {
7777
mpmArguments = mpmArguments.concat(["--release-status=Prerelease"]);
7878
}
@@ -89,3 +89,30 @@ export async function install(mpmPath: string, release: matlab.Release, products
8989
}
9090
return
9191
}
92+
93+
export async function installFromSource(mpmPath: string, source: string, products: string[], destination: string) {
94+
// remove spaces and flatten product list
95+
let parsedProducts = products.flatMap(p => p.split(/[ ]+/));
96+
// Add MATLAB by default
97+
parsedProducts.push("MATLAB");
98+
// Remove duplicate products
99+
parsedProducts = [...new Set(parsedProducts)];
100+
101+
let mpmArguments: string[] = [
102+
"install",
103+
`--source=${source}`,
104+
`--destination=${destination}`,
105+
];
106+
mpmArguments = mpmArguments.concat("--products", ...parsedProducts);
107+
108+
const exitCode = await exec.exec(mpmPath, mpmArguments).catch(async e => {
109+
// Fully remove failed MATLAB installation for self-hosted runners
110+
await rmRF(destination);
111+
throw e;
112+
});
113+
if (exitCode !== 0) {
114+
await rmRF(destination);
115+
return Promise.reject(Error(`Script exited with non-zero code ${exitCode}`));
116+
}
117+
return
118+
}

src/mpm.unit.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,38 @@ describe("mpm install", () => {
180180
await expect(mpm.install(mpmPath, releaseInfo, products, destination)).rejects.toBeDefined();
181181
expect(rmRFMock).toHaveBeenCalledWith(destination);
182182
});
183+
184+
it("ideally works when installing from source", async () => {
185+
const destination ="/opt/matlab";
186+
const source = "/path/to/source";
187+
const products = ["MATLAB", "Compiler"];
188+
const expectedMpmArgs = [
189+
"install",
190+
`--source=${source}`,
191+
`--destination=${destination}`,
192+
"--products",
193+
"MATLAB",
194+
"Compiler",
195+
]
196+
execMock.mockResolvedValue(0);
197+
198+
await expect(mpm.installFromSource(mpmPath, source, products, destination)).resolves.toBeUndefined();
199+
expect(execMock.mock.calls[0][1]).toMatchObject(expectedMpmArgs);
200+
});
201+
202+
it("rejects and cleans on mpm rejection when installing from source", async () => {
203+
const destination = "/opt/matlab";
204+
const products = ["MATLAB", "Compiler"];
205+
execMock.mockRejectedValue(1);
206+
await expect(mpm.installFromSource(mpmPath, "/path", products, destination)).rejects.toBeDefined();
207+
expect(rmRFMock).toHaveBeenCalledWith(destination);
208+
});
209+
210+
it("rejects and cleans on failed install when installing from source", async () => {
211+
const destination = "/opt/matlab";
212+
const products = ["MATLAB", "Compiler"];
213+
execMock.mockResolvedValue(1);
214+
await expect(mpm.installFromSource(mpmPath, "/path", products, destination)).rejects.toBeDefined();
215+
expect(rmRFMock).toHaveBeenCalledWith(destination);
216+
});
183217
});

0 commit comments

Comments
 (0)