2
2
3
3
const fs = require ( 'graceful-fs' )
4
4
const os = require ( 'os' )
5
+ const { backOff } = require ( 'exponential-backoff' )
6
+ const rm = require ( 'rimraf' )
5
7
const tar = require ( 'tar' )
6
8
const path = require ( 'path' )
7
9
const util = require ( 'util' )
@@ -98,6 +100,40 @@ async function install (fs, gyp, argv) {
98
100
}
99
101
}
100
102
103
+ async function copyDirectory ( src , dest ) {
104
+ try {
105
+ await fs . promises . stat ( src )
106
+ } catch {
107
+ throw new Error ( `Missing source directory for copy: ${ src } ` )
108
+ }
109
+ await fs . promises . mkdir ( dest , { recursive : true } )
110
+ const entries = await fs . promises . readdir ( src , { withFileTypes : true } )
111
+ for ( const entry of entries ) {
112
+ if ( entry . isDirectory ( ) ) {
113
+ await copyDirectory ( path . join ( src , entry . name ) , path . join ( dest , entry . name ) )
114
+ } else if ( entry . isFile ( ) ) {
115
+ // with parallel installs, copying files may cause file errors on
116
+ // Windows so use an exponential backoff to resolve collisions
117
+ await backOff ( async ( ) => {
118
+ try {
119
+ await fs . promises . copyFile ( path . join ( src , entry . name ) , path . join ( dest , entry . name ) )
120
+ } catch ( err ) {
121
+ // if ensure, check if file already exists and that's good enough
122
+ if ( gyp . opts . ensure && err . code === 'EBUSY' ) {
123
+ try {
124
+ await fs . promises . stat ( path . join ( dest , entry . name ) )
125
+ return
126
+ } catch { }
127
+ }
128
+ throw err
129
+ }
130
+ } )
131
+ } else {
132
+ throw new Error ( 'Unexpected file directory entry type' )
133
+ }
134
+ }
135
+ }
136
+
101
137
async function go ( ) {
102
138
log . verbose ( 'ensuring nodedir is created' , devDir )
103
139
@@ -118,6 +154,7 @@ async function install (fs, gyp, argv) {
118
154
119
155
// now download the node tarball
120
156
const tarPath = gyp . opts . tarball
157
+ let extractErrors = false
121
158
let extractCount = 0
122
159
const contentShasums = { }
123
160
const expectShasums = { }
@@ -136,71 +173,99 @@ async function install (fs, gyp, argv) {
136
173
return isValid
137
174
}
138
175
176
+ function onwarn ( code , message ) {
177
+ extractErrors = true
178
+ log . error ( 'error while extracting tarball' , code , message )
179
+ }
180
+
139
181
// download the tarball and extract!
140
182
141
- if ( tarPath ) {
142
- await tar . extract ( {
143
- file : tarPath ,
144
- strip : 1 ,
145
- filter : isValid ,
146
- cwd : devDir
147
- } )
148
- } else {
149
- try {
150
- const res = await download ( gyp , release . tarballUrl )
183
+ // on Windows there can be file errors from tar if parallel installs
184
+ // are happening (not uncommon with multiple native modules) so
185
+ // extract the tarball to a temp directory first and then copy over
186
+ const tarExtractDir = win ? await fs . promises . mkdtemp ( path . join ( os . tmpdir ( ) , 'node-gyp-tmp-' ) ) : devDir
151
187
152
- if ( res . status !== 200 ) {
153
- throw new Error ( `${ res . status } response downloading ${ release . tarballUrl } ` )
154
- }
188
+ try {
189
+ if ( tarPath ) {
190
+ await tar . extract ( {
191
+ file : tarPath ,
192
+ strip : 1 ,
193
+ filter : isValid ,
194
+ onwarn,
195
+ cwd : tarExtractDir
196
+ } )
197
+ } else {
198
+ try {
199
+ const res = await download ( gyp , release . tarballUrl )
155
200
156
- await streamPipeline (
157
- res . body ,
158
- // content checksum
159
- new ShaSum ( ( _ , checksum ) => {
160
- const filename = path . basename ( release . tarballUrl ) . trim ( )
161
- contentShasums [ filename ] = checksum
162
- log . verbose ( 'content checksum' , filename , checksum )
163
- } ) ,
164
- tar . extract ( {
165
- strip : 1 ,
166
- cwd : devDir ,
167
- filter : isValid
168
- } )
169
- )
170
- } catch ( err ) {
171
- // something went wrong downloading the tarball?
172
- if ( err . code === 'ENOTFOUND' ) {
173
- throw new Error ( 'This is most likely not a problem with node-gyp or the package itself and\n' +
174
- 'is related to network connectivity. In most cases you are behind a proxy or have bad \n' +
175
- 'network settings.' )
201
+ if ( res . status !== 200 ) {
202
+ throw new Error ( `${ res . status } response downloading ${ release . tarballUrl } ` )
203
+ }
204
+
205
+ await streamPipeline (
206
+ res . body ,
207
+ // content checksum
208
+ new ShaSum ( ( _ , checksum ) => {
209
+ const filename = path . basename ( release . tarballUrl ) . trim ( )
210
+ contentShasums [ filename ] = checksum
211
+ log . verbose ( 'content checksum' , filename , checksum )
212
+ } ) ,
213
+ tar . extract ( {
214
+ strip : 1 ,
215
+ cwd : tarExtractDir ,
216
+ filter : isValid ,
217
+ onwarn
218
+ } )
219
+ )
220
+ } catch ( err ) {
221
+ // something went wrong downloading the tarball?
222
+ if ( err . code === 'ENOTFOUND' ) {
223
+ throw new Error ( 'This is most likely not a problem with node-gyp or the package itself and\n' +
224
+ 'is related to network connectivity. In most cases you are behind a proxy or have bad \n' +
225
+ 'network settings.' )
226
+ }
227
+ throw err
176
228
}
177
- throw err
178
229
}
179
- }
180
230
181
- // invoked after the tarball has finished being extracted
182
- if ( extractCount === 0 ) {
183
- throw new Error ( 'There was a fatal problem while downloading/extracting the tarball' )
184
- }
231
+ // invoked after the tarball has finished being extracted
232
+ if ( extractErrors || extractCount === 0 ) {
233
+ throw new Error ( 'There was a fatal problem while downloading/extracting the tarball' )
234
+ }
235
+
236
+ log . verbose ( 'tarball' , 'done parsing tarball' )
237
+
238
+ const installVersionPath = path . resolve ( tarExtractDir , 'installVersion' )
239
+ await Promise . all ( [
240
+ // need to download node.lib
241
+ ...( win ? downloadNodeLib ( ) : [ ] ) ,
242
+ // write the "installVersion" file
243
+ fs . promises . writeFile ( installVersionPath , gyp . package . installVersion + '\n' ) ,
244
+ // Only download SHASUMS.txt if we downloaded something in need of SHA verification
245
+ ...( ! tarPath || win ? [ downloadShasums ( ) ] : [ ] )
246
+ ] )
247
+
248
+ log . verbose ( 'download contents checksum' , JSON . stringify ( contentShasums ) )
249
+ // check content shasums
250
+ for ( const k in contentShasums ) {
251
+ log . verbose ( 'validating download checksum for ' + k , '(%s == %s)' , contentShasums [ k ] , expectShasums [ k ] )
252
+ if ( contentShasums [ k ] !== expectShasums [ k ] ) {
253
+ throw new Error ( k + ' local checksum ' + contentShasums [ k ] + ' not match remote ' + expectShasums [ k ] )
254
+ }
255
+ }
185
256
186
- log . verbose ( 'tarball' , 'done parsing tarball' )
187
-
188
- const installVersionPath = path . resolve ( devDir , 'installVersion' )
189
- await Promise . all ( [
190
- // need to download node.lib
191
- ...( win ? downloadNodeLib ( ) : [ ] ) ,
192
- // write the "installVersion" file
193
- fs . promises . writeFile ( installVersionPath , gyp . package . installVersion + '\n' ) ,
194
- // Only download SHASUMS.txt if we downloaded something in need of SHA verification
195
- ...( ! tarPath || win ? [ downloadShasums ( ) ] : [ ] )
196
- ] )
197
-
198
- log . verbose ( 'download contents checksum' , JSON . stringify ( contentShasums ) )
199
- // check content shasums
200
- for ( const k in contentShasums ) {
201
- log . verbose ( 'validating download checksum for ' + k , '(%s == %s)' , contentShasums [ k ] , expectShasums [ k ] )
202
- if ( contentShasums [ k ] !== expectShasums [ k ] ) {
203
- throw new Error ( k + ' local checksum ' + contentShasums [ k ] + ' not match remote ' + expectShasums [ k ] )
257
+ // copy over the files from the temp tarball extract directory to devDir
258
+ if ( tarExtractDir !== devDir ) {
259
+ await copyDirectory ( tarExtractDir , devDir )
260
+ }
261
+ } finally {
262
+ if ( tarExtractDir !== devDir ) {
263
+ try {
264
+ // try to cleanup temp dir
265
+ await util . promisify ( rm ) ( tarExtractDir )
266
+ } catch {
267
+ log . warn ( 'failed to clean up temp tarball extract directory' )
268
+ }
204
269
}
205
270
}
206
271
@@ -232,7 +297,7 @@ async function install (fs, gyp, argv) {
232
297
log . verbose ( 'on Windows; need to download `' + release . name + '.lib`...' )
233
298
const archs = [ 'ia32' , 'x64' , 'arm64' ]
234
299
return archs . map ( async ( arch ) => {
235
- const dir = path . resolve ( devDir , arch )
300
+ const dir = path . resolve ( tarExtractDir , arch )
236
301
const targetLibPath = path . resolve ( dir , release . name + '.lib' )
237
302
const { libUrl, libPath } = release [ arch ]
238
303
const name = `${ arch } ${ release . name } .lib`
0 commit comments