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
719import nodeOs from 'os' ;
8- import { existsSync , mkdirSync , readFileSync , renameSync , rmSync , writeFileSync } from 'fs' ;
20+ import { cpSync , existsSync , mkdirSync , readFileSync , rmSync , writeFileSync } from 'fs' ;
921import path from 'path' ;
1022import { downloadFile } from './file-download' ;
1123import yargs from 'yargs' ;
1224import extractZip from 'extract-zip' ;
25+ import { execSync } from 'child_process' ;
1326
1427// OS/architecture pairs from vsce --publish
1528type VsceTarget = 'win32-x64' | 'win32-arm64' | 'linux-x64' | 'linux-arm64' | 'darwin-x64' | 'darwin-arm64' ;
1629const 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+
1837const TOOLS = {
1938 'pyocd' : downloadPyOCD ,
2039} ;
@@ -24,30 +43,105 @@ const PACKAGE_JSON = path.resolve(__dirname, '../package.json');
2443function 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+
3660async 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
109195async 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
0 commit comments