@@ -15,7 +15,7 @@ import * as httpUtils from './httpUtils
15
15
import * as nodeUtils from './nodeUtils' ;
16
16
import * as npmRegistryUtils from './npmRegistryUtils' ;
17
17
import { RegistrySpec , Descriptor , Locator , PackageManagerSpec } from './types' ;
18
- import { BinList , BinSpec , InstallSpec } from './types' ;
18
+ import { BinList , BinSpec , InstallSpec , DownloadSpec } from './types' ;
19
19
20
20
export function getRegistryFromPackageManagerSpec ( spec : PackageManagerSpec ) {
21
21
return process . env . COREPACK_NPM_REGISTRY
@@ -132,6 +132,66 @@ function isValidBinSpec(x: unknown): x is BinSpec {
132
132
return typeof x === `object` && x !== null && ! Array . isArray ( x ) && Object . keys ( x ) . length > 0 ;
133
133
}
134
134
135
+ async function download ( installTarget : string , url : string , algo : string , binPath : string | null = null ) : Promise < DownloadSpec > {
136
+ // Creating a temporary folder inside the install folder means that we
137
+ // are sure it'll be in the same drive as the destination, so we can
138
+ // just move it there atomically once we are done
139
+
140
+ const tmpFolder = folderUtils . getTemporaryFolder ( installTarget ) ;
141
+ debugUtils . log ( `Downloading to ${ tmpFolder } ` ) ;
142
+
143
+ const stream = await httpUtils . fetchUrlStream ( url ) ;
144
+
145
+ const parsedUrl = new URL ( url ) ;
146
+ const ext = path . posix . extname ( parsedUrl . pathname ) ;
147
+
148
+ let outputFile : string | null = null ;
149
+ let sendTo : any ;
150
+
151
+ if ( ext === `.tgz` ) {
152
+ const { default : tar } = await import ( `tar` ) ;
153
+ sendTo = tar . x ( {
154
+ strip : 1 ,
155
+ cwd : tmpFolder ,
156
+ filter : binPath ? path => {
157
+ const pos = path . indexOf ( `/` ) ;
158
+ return pos !== - 1 && path . slice ( pos + 1 ) === binPath ;
159
+ } : undefined ,
160
+ } ) ;
161
+ } else if ( ext === `.js` ) {
162
+ outputFile = path . join ( tmpFolder , path . posix . basename ( parsedUrl . pathname ) ) ;
163
+ sendTo = fs . createWriteStream ( outputFile ) ;
164
+ }
165
+ stream . pipe ( sendTo ) ;
166
+
167
+ let hash = ! binPath ? stream . pipe ( createHash ( algo ) ) : null ;
168
+ await once ( sendTo , `finish` ) ;
169
+
170
+ if ( binPath ) {
171
+ const downloadedBin = path . join ( tmpFolder , binPath ) ;
172
+ outputFile = path . join ( tmpFolder , path . basename ( downloadedBin ) ) ;
173
+ try {
174
+ await renameSafe ( downloadedBin , outputFile ) ;
175
+ } catch ( err ) {
176
+ if ( ( err as nodeUtils . NodeError ) ?. code === `ENOENT` )
177
+ throw new Error ( `Cannot locate '${ binPath } ' in downloaded tarball` , { cause : err } ) ;
178
+
179
+ throw err ;
180
+ }
181
+
182
+ // Calculate the hash of the bin file
183
+ const fileStream = fs . createReadStream ( outputFile ) ;
184
+ hash = fileStream . pipe ( createHash ( algo ) ) ;
185
+ await once ( fileStream , `close` ) ;
186
+ }
187
+
188
+ return {
189
+ tmpFolder,
190
+ outputFile,
191
+ hash : hash ! . digest ( `hex` ) ,
192
+ } ;
193
+ }
194
+
135
195
export async function installVersion ( installTarget : string , locator : Locator , { spec} : { spec : PackageManagerSpec } ) : Promise < InstallSpec > {
136
196
const locatorIsASupportedPackageManager = isSupportedPackageManagerLocator ( locator ) ;
137
197
const locatorReference = locatorIsASupportedPackageManager ? semver . parse ( locator . reference ) ! : parseURLReference ( locator ) ;
@@ -159,12 +219,16 @@ export async function installVersion(installTarget: string, locator: Locator, {s
159
219
}
160
220
161
221
let url : string ;
222
+ let binPath : string | null = null ;
162
223
if ( locatorIsASupportedPackageManager ) {
163
224
url = spec . url . replace ( `{}` , version ) ;
164
225
if ( process . env . COREPACK_NPM_REGISTRY ) {
165
226
const registry = getRegistryFromPackageManagerSpec ( spec ) ;
166
227
if ( registry . type === `npm` ) {
167
228
url = await npmRegistryUtils . fetchTarballUrl ( registry . package , version ) ;
229
+ if ( registry . bin ) {
230
+ binPath = registry . bin ;
231
+ }
168
232
} else {
169
233
url = url . replace (
170
234
npmRegistryUtils . DEFAULT_NPM_REGISTRY_URL ,
@@ -182,33 +246,9 @@ export async function installVersion(installTarget: string, locator: Locator, {s
182
246
}
183
247
}
184
248
185
- // Creating a temporary folder inside the install folder means that we
186
- // are sure it'll be in the same drive as the destination, so we can
187
- // just move it there atomically once we are done
188
-
189
- const tmpFolder = folderUtils . getTemporaryFolder ( installTarget ) ;
190
- debugUtils . log ( `Installing ${ locator . name } @${ version } from ${ url } to ${ tmpFolder } ` ) ;
191
- const stream = await httpUtils . fetchUrlStream ( url ) ;
192
-
193
- const parsedUrl = new URL ( url ) ;
194
- const ext = path . posix . extname ( parsedUrl . pathname ) ;
195
-
196
- let outputFile : string | null = null ;
197
-
198
- let sendTo : any ;
199
- if ( ext === `.tgz` ) {
200
- const { default : tar } = await import ( `tar` ) ;
201
- sendTo = tar . x ( { strip : 1 , cwd : tmpFolder } ) ;
202
- } else if ( ext === `.js` ) {
203
- outputFile = path . join ( tmpFolder , path . posix . basename ( parsedUrl . pathname ) ) ;
204
- sendTo = fs . createWriteStream ( outputFile ) ;
205
- }
206
-
207
- stream . pipe ( sendTo ) ;
208
-
249
+ debugUtils . log ( `Installing ${ locator . name } @${ version } from ${ url } ` ) ;
209
250
const algo = build [ 0 ] ?? `sha256` ;
210
- const hash = stream . pipe ( createHash ( algo ) ) ;
211
- await once ( sendTo , `finish` ) ;
251
+ const { tmpFolder, outputFile, hash : actualHash } = await download ( installTarget , url , algo , binPath ) ;
212
252
213
253
let bin : BinSpec | BinList ;
214
254
const isSingleFile = outputFile !== null ;
@@ -240,7 +280,6 @@ export async function installVersion(installTarget: string, locator: Locator, {s
240
280
}
241
281
}
242
282
243
- const actualHash = hash . digest ( `hex` ) ;
244
283
if ( build [ 1 ] && actualHash !== build [ 1 ] )
245
284
throw new Error ( `Mismatch hashes. Expected ${ build [ 1 ] } , got ${ actualHash } ` ) ;
246
285
@@ -305,6 +344,14 @@ export async function installVersion(installTarget: string, locator: Locator, {s
305
344
} ;
306
345
}
307
346
347
+ async function renameSafe ( oldPath : fs . PathLike , newPath : fs . PathLike ) {
348
+ if ( process . platform === `win32` ) {
349
+ await renameUnderWindows ( oldPath , newPath ) ;
350
+ } else {
351
+ await fs . promises . rename ( oldPath , newPath ) ;
352
+ }
353
+ }
354
+
308
355
async function renameUnderWindows ( oldPath : fs . PathLike , newPath : fs . PathLike ) {
309
356
// Windows malicious file analysis blocks files currently under analysis, so we need to wait for file release
310
357
const retries = 5 ;
0 commit comments