Skip to content

Commit e86ef8f

Browse files
author
Your Name
committed
Further updates:
- Apache 2.0 copyright - Address review comments - Use yarn cache for tool downloads - Check tool sha256 if available
1 parent 87979db commit e86ef8f

File tree

7 files changed

+177
-50
lines changed

7 files changed

+177
-50
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ jobs:
7373
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
7474
with:
7575
name: dist
76+
path: dist
7677

7778
- name: Cache tools
7879
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.6

.vscodeignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ webpack.config.js
1414
scripts
1515
tools/**/version.txt
1616
tools/**/target.txt
17+
tools/**/sha256.txt

DEVELOPMENT.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,15 @@ Supported `<target>`s are:
6767
```
6868
6969
**Note**: At this point, no tests have been added to this repository.
70+
71+
## Updating tool dependencies
72+
73+
Tool dependencies are recorded in `package.json`:
74+
75+
```json
76+
"cmsis": {
77+
"<tool>": "[<owner>/<repo>@]<version>"
78+
}
79+
````
80+
81+
The `<version>` must match the tools release version tag.

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default [
2121
{
2222
ignores: [
2323
"dist",
24+
"scripts",
2425
"**/*.d.ts",
2526
"jest.config.js",
2627
"node_modules",

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@
223223
"scripts": {
224224
"prepare": "yarn run build",
225225
"download-tools": "ts-node scripts/download-tools.ts",
226-
"clean": "git clean -f -x ./node_modules ./dist ./coverage",
226+
"clean": "git clean -f -x ./node_modules ./dist ./coverage ./tools",
227227
"build": "webpack --mode production && yarn lint",
228228
"watch": "webpack -w",
229229
"lint": "eslint .",
@@ -259,6 +259,6 @@
259259
"yargs": "^17.7.2"
260260
},
261261
"cmsis": {
262-
"pyocd": "0.0.0-standalone"
262+
"pyocd": "MatthiasHertel80/pyOCD@0.0.0-standalone-cbuild1"
263263
}
264264
}

scripts/download-tools.ts

Lines changed: 147 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,39 @@
11
#!npx ts-node
22

3-
/*
4-
* Copyright (C) 2025 Arm Limited
3+
/**
4+
* Copyright 2025 Arm Limited
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
517
*/
618

719
import nodeOs from 'os';
8-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'fs';
20+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
921
import path from 'path';
1022
import { downloadFile } from './file-download';
1123
import yargs from 'yargs';
1224
import extractZip from 'extract-zip';
25+
import { execSync } from 'child_process';
1326

1427
// OS/architecture pairs from vsce --publish
1528
type VsceTarget = 'win32-x64' | 'win32-arm64' | 'linux-x64' | 'linux-arm64' | 'darwin-x64' | 'darwin-arm64';
1629
const VSCE_TARGETS = ['win32-x64', 'win32-arm64', 'linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64'] as const;
1730

31+
type Repo = { repo: string };
32+
type Owner = { owner: string };
33+
type Token = { token: string };
34+
35+
type ToolOptions = { token?: string, cache?: string };
36+
1837
const TOOLS = {
1938
'pyocd': downloadPyOCD,
2039
};
@@ -24,30 +43,105 @@ const PACKAGE_JSON = path.resolve(__dirname, '../package.json');
2443
function getVersionFromPackageJson(packageJsonPath: string, tool: keyof typeof TOOLS) {
2544
const packageJsonContent = readFileSync(packageJsonPath, 'utf-8');
2645
const packageJson = JSON.parse(packageJsonContent);
27-
const cmsisConfig = packageJson.cmsis;
28-
29-
return `${cmsisConfig[tool]}`;
46+
return packageJson?.cmsis[tool] as string | undefined;
3047
}
3148

32-
function getVersionCore(version: string) {
33-
return version.split('-')[0];
49+
function splitGitReleaseVersion(releaseVersion: string, repoAndOwnerDefault: Repo & Owner) {
50+
if (releaseVersion.includes('@')) {
51+
const parts = releaseVersion.split('@');
52+
const version = parts[1];
53+
const repoAndOwner = parts[0].split('/');
54+
return { repoAndOwner: { owner: repoAndOwner[0], repo: repoAndOwner[1] }, version };
55+
}
56+
return { repoAndOwner: repoAndOwnerDefault, version: releaseVersion };
3457
}
3558

59+
3660
async function createOktokit(auth?: string) {
3761
const { Octokit } = await import('octokit');
3862

3963
const { default: nodeFetch } = await import('node-fetch');
4064
return new Octokit({ auth, request: { fetch: nodeFetch } });
4165
}
4266

43-
async function downloadPyOCD(target: VsceTarget, dest: string) {
44-
const repoAndOwner = { owner: 'MatthiasHertel80', repo: 'pyOCD' } as const;
67+
async function findGithubReleaseAsset(repo: Repo & Owner & Partial<Token>, version: string, asset_name: string) {
68+
const repoAndOwner = { owner: repo.owner, repo: repo.repo };
69+
const octokit = await createOktokit(repo.token);
70+
71+
const releases = (await octokit.rest.repos.listReleases({ ...repoAndOwner })).data;
72+
const release = releases.find(r => r.tag_name === `v${version}` || r.tag_name === version);
73+
74+
if (!release) {
75+
throw new Error(`Could not find release for version ${version}`);
76+
}
77+
78+
const assets = (await octokit.rest.repos.listReleaseAssets({ ...repoAndOwner, release_id: release.id })).data;
79+
const asset = assets.find(a => a.name === asset_name);
80+
81+
if (!asset) {
82+
throw new Error(`Could not find release asset for version ${version}`);
83+
}
84+
85+
const asset_sha256 = assets.find(a => a.name === `${asset_name}.sha256`);
86+
87+
return { asset, sha256: asset_sha256 };
88+
}
89+
90+
async function retrieveSha256(url?: string, token?: string) {
91+
if (url) {
92+
const tempfile = await import('tempfile');
93+
const downloadFilePath = tempfile.default({ extension: '.sha256' });
94+
console.debug(`Downloading ${url} ...`);
95+
await downloadFile(url, downloadFilePath, token);
96+
const sha256 = readFileSync(downloadFilePath, { encoding: 'utf8' });
97+
rmSync(downloadFilePath, { force: true });
98+
return sha256;
99+
}
100+
return undefined;
101+
}
102+
103+
async function download(url: string, options?: ToolOptions & { cache_key?: string }) {
104+
const cachePath = (options?.cache && options?.cache_key) ? path.join(options.cache, options.cache_key): undefined;
105+
if (cachePath && existsSync(cachePath)) {
106+
console.debug(`Found asset in cache ${cachePath} ...`);
107+
return { mode: 'cache', path: cachePath};
108+
}
109+
110+
const tempfile = await import('tempfile');
111+
const downloadFilePath = tempfile.default({ extension: '.zip' });
112+
console.debug(`Downloading ${url} ...`);
113+
await downloadFile(url, downloadFilePath, options?.token).catch(error => {
114+
throw new Error(`Failed to download ${url}`, { cause: error });
115+
});
116+
117+
const extractPath = cachePath ?? downloadFilePath.replace('.zip', '');
118+
console.debug(`Extracting to ${extractPath} ...`);
119+
await extractZip(downloadFilePath, { dir: extractPath }).catch(error => {
120+
throw new Error(`Failed to extract ${url}`, { cause: error });
121+
});
122+
123+
rmSync(downloadFilePath, { force: true });
124+
return { mode: cachePath ? 'cache' : 'temp', path: extractPath };
125+
}
126+
127+
async function downloadPyOCD(target: VsceTarget, dest: string, options?: ToolOptions) {
128+
const repoAndOwnerDefault = { owner: 'MatthiasHertel80', repo: 'pyOCD' } as const;
129+
const jsonVersion = getVersionFromPackageJson(PACKAGE_JSON, 'pyocd');
130+
131+
if (!jsonVersion) {
132+
throw new Error('PyOCD version not found in package.json');
133+
}
134+
135+
console.log(`Looking up PyOCD version ${jsonVersion} (${target}) ...`);
136+
137+
const { repoAndOwner, version } = splitGitReleaseVersion(jsonVersion, repoAndOwnerDefault);
138+
45139
const githubToken = process.env.GITHUB_TOKEN;
46140
const destPath = path.join(dest, 'pyocd');
47141
const versionFilePath = path.join(destPath, 'version.txt');
48142
const targetFilePath = path.join(destPath, 'target.txt');
143+
const sha256FilePath = path.join(destPath, 'sha256.txt');
49144

50-
const version = getVersionFromPackageJson(PACKAGE_JSON, 'pyocd');
51145
const { os, arch } = {
52146
'win32-x64': { os: 'windows', arch: '' },
53147
'win32-arm64': { os: 'windows', arch: '' },
@@ -57,57 +151,53 @@ async function downloadPyOCD(target: VsceTarget, dest: string) {
57151
'darwin-arm64': { os: 'macos', arch: '' },
58152
}[target];
59153

154+
const asset_name = `pyocd-${os}${arch}-${version}.zip`;
155+
console.debug(`Looking up GitHub release asset ${repoAndOwner.owner}/${repoAndOwner.repo}/${version}/${asset_name} ...`);
156+
const { asset, sha256 } = await findGithubReleaseAsset({ ...repoAndOwner, token: githubToken }, version, asset_name);
157+
const sha256sum = await retrieveSha256(sha256?.url, githubToken).catch(error => {
158+
console.warn(`Failed to retrieve sha256 sum: ${error}`);
159+
return undefined;
160+
});
161+
60162
if (existsSync(versionFilePath) && existsSync(targetFilePath)) {
61163
const hasVersion = readFileSync(versionFilePath, { encoding: 'utf8' });
62164
const hasTarget = readFileSync(targetFilePath, { encoding: 'utf8' });
63-
if (version === hasVersion && target === hasTarget) {
64-
console.log(`PyOCD version ${version} (${target}) already available.`);
165+
const hasSha256Sum = existsSync(sha256FilePath) ? readFileSync(sha256FilePath, { encoding: 'utf8' }) : undefined;
166+
167+
if (jsonVersion === hasVersion && target === hasTarget && ((sha256sum === undefined) || (sha256sum === hasSha256Sum))) {
168+
console.log(`PyOCD version ${jsonVersion} (${target}) already available.`);
65169
return;
66170
}
67171
}
68172

69-
console.log(`Downloading PyOCD version ${version} (${target}) ...`);
70-
71-
const octokit = await createOktokit(githubToken);
72-
73-
const releases = (await octokit.rest.repos.listReleases(repoAndOwner)).data;
74-
const release = releases.find(r => r.tag_name === `v${version}` || r.tag_name === version);
75-
76-
if (!release) {
77-
throw new Error(`Could not find release for version ${version}`);
78-
}
79-
80-
const assets = (await octokit.rest.repos.listReleaseAssets({ ...repoAndOwner, release_id: release.id })).data;
81-
const asset = assets.find(a => a.name === `pyocd-${os}${arch}-${getVersionCore(version)}.zip`);
173+
const { mode, path: extractPath } = await download(asset.url, { token: githubToken, cache: options?.cache, cache_key: `cmsis-pyocd-${version}-${sha256sum}` });
82174

83-
if (!asset) {
84-
throw new Error(`Could not find release asset for version ${version} and target ${target}`);
175+
if (existsSync(destPath)) {
176+
console.debug(`Removing existing ${destPath} ...`);
177+
rmSync(destPath, { recursive: true, force: true });
85178
}
86179

87-
const tempfile = await import('tempfile');
88-
const downloadFilePath = tempfile.default({ extension: '.zip' });
89-
await downloadFile(asset.url, downloadFilePath, githubToken).catch(error => {
90-
throw new Error(`Failed to download PyOCD: ${error}`);
91-
});
92-
93-
const extractPath = downloadFilePath.replace('.zip', '');
94-
await extractZip(downloadFilePath, { dir: extractPath }).catch(error => {
95-
throw new Error(`Failed to extract PyOCD: ${error}`);
96-
});
180+
console.debug(`Copying ${extractPath} to ${destPath} ...`);
181+
cpSync(extractPath, destPath, { recursive: true, force: true });
97182

98-
rmSync(downloadFilePath, { force: true });
99-
100-
if (existsSync(destPath)) {
101-
rmSync(destPath, { recursive: true, force: true });
183+
if (mode === 'temp') {
184+
console.debug(`Removing temporary ${extractPath} ...`);
185+
rmSync(extractPath, { recursive: true, force: true });
102186
}
103-
renameSync(extractPath, destPath);
104187

105-
writeFileSync(versionFilePath, version, { encoding: 'utf8' });
188+
writeFileSync(versionFilePath, jsonVersion, { encoding: 'utf8' });
106189
writeFileSync(targetFilePath, target, { encoding: 'utf8' });
190+
if (sha256sum) {
191+
writeFileSync(sha256FilePath, sha256sum, { encoding: 'utf8' });
192+
}
107193
}
108194

109195
async function main() {
110-
const { target, dest, tools } = yargs
196+
// Get Yarn cache directory
197+
const yarnCacheDir = execSync('yarn cache dir').toString().trim();
198+
console.debug(`Yarn cache directory: ${yarnCacheDir}`);
199+
200+
const { target, dest, cache, cache_disable, tools } = yargs
111201
.option('t', {
112202
alias: 'target',
113203
description: 'VS Code extension target, defaults to system',
@@ -119,6 +209,16 @@ async function main() {
119209
description: 'Destination directory for the tools',
120210
default: path.join(__dirname, '..', 'tools')
121211
})
212+
.option('c', {
213+
alias: 'cache',
214+
description: 'Directory for caching tool downloads',
215+
default: yarnCacheDir
216+
})
217+
.option('no-cache', {
218+
description: 'Disable caching of tool downloads',
219+
type: 'boolean',
220+
default: false
221+
})
122222
.version(false)
123223
.strict()
124224
.command('$0 [<tools> ...]', 'Downloads the tool(s) for the given architecture and OS', y => {
@@ -129,14 +229,14 @@ async function main() {
129229
default: Object.keys(TOOLS)
130230
});
131231
})
132-
.argv as unknown as { target: VsceTarget, dest: string, tools: (keyof typeof TOOLS)[] };
232+
.argv as unknown as { target: VsceTarget, dest: string, cache: string, cache_disable: boolean, tools: (keyof typeof TOOLS)[] };
133233

134234
if (!existsSync(dest)) {
135235
mkdirSync(dest, { recursive: true });
136236
}
137237

138238
for (const tool of new Set(tools)) {
139-
TOOLS[tool](target, dest);
239+
TOOLS[tool](target, dest, { cache: cache_disable ? undefined : cache });
140240
}
141241
}
142242

scripts/file-download.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
/**
2-
* Copyright (C) 2024 Arm Limited
2+
* Copyright 2025 Arm Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
315
*/
416

517
import { createWriteStream } from 'fs';

0 commit comments

Comments
 (0)