@@ -15,7 +15,7 @@ import * as httpUtils from './httpUtils
1515import * as nodeUtils from './nodeUtils' ;
1616import * as npmRegistryUtils from './npmRegistryUtils' ;
1717import { RegistrySpec , Descriptor , Locator , PackageManagerSpec } from './types' ;
18- import { BinList , BinSpec , InstallSpec } from './types' ;
18+ import { BinList , BinSpec , InstallSpec , DownloadSpec } from './types' ;
1919
2020export function getRegistryFromPackageManagerSpec ( spec : PackageManagerSpec ) {
2121 return process . env . COREPACK_NPM_REGISTRY
@@ -132,6 +132,66 @@ function isValidBinSpec(x: unknown): x is BinSpec {
132132 return typeof x === `object` && x !== null && ! Array . isArray ( x ) && Object . keys ( x ) . length > 0 ;
133133}
134134
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+
135195export async function installVersion ( installTarget : string , locator : Locator , { spec} : { spec : PackageManagerSpec } ) : Promise < InstallSpec > {
136196 const locatorIsASupportedPackageManager = isSupportedPackageManagerLocator ( locator ) ;
137197 const locatorReference = locatorIsASupportedPackageManager ? semver . parse ( locator . reference ) ! : parseURLReference ( locator ) ;
@@ -159,12 +219,16 @@ export async function installVersion(installTarget: string, locator: Locator, {s
159219 }
160220
161221 let url : string ;
222+ let binPath : string | null = null ;
162223 if ( locatorIsASupportedPackageManager ) {
163224 url = spec . url . replace ( `{}` , version ) ;
164225 if ( process . env . COREPACK_NPM_REGISTRY ) {
165226 const registry = getRegistryFromPackageManagerSpec ( spec ) ;
166227 if ( registry . type === `npm` ) {
167228 url = await npmRegistryUtils . fetchTarballUrl ( registry . package , version ) ;
229+ if ( registry . bin ) {
230+ binPath = registry . bin ;
231+ }
168232 } else {
169233 url = url . replace (
170234 npmRegistryUtils . DEFAULT_NPM_REGISTRY_URL ,
@@ -182,33 +246,9 @@ export async function installVersion(installTarget: string, locator: Locator, {s
182246 }
183247 }
184248
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 } ` ) ;
209250 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 ) ;
212252
213253 let bin : BinSpec | BinList ;
214254 const isSingleFile = outputFile !== null ;
@@ -240,7 +280,6 @@ export async function installVersion(installTarget: string, locator: Locator, {s
240280 }
241281 }
242282
243- const actualHash = hash . digest ( `hex` ) ;
244283 if ( build [ 1 ] && actualHash !== build [ 1 ] )
245284 throw new Error ( `Mismatch hashes. Expected ${ build [ 1 ] } , got ${ actualHash } ` ) ;
246285
@@ -305,6 +344,14 @@ export async function installVersion(installTarget: string, locator: Locator, {s
305344 } ;
306345}
307346
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+
308355async function renameUnderWindows ( oldPath : fs . PathLike , newPath : fs . PathLike ) {
309356 // Windows malicious file analysis blocks files currently under analysis, so we need to wait for file release
310357 const retries = 5 ;
0 commit comments