1
- import { execSync } from "node:child_process" ;
2
1
import fs from "node:fs" ;
3
2
import path from 'node:path' ;
4
3
import os , { EOL } from "os" ;
5
- import { environmentVariableIsPopulated , getCustom , handleSpacesInPath } from "../tools.js" ;
6
-
4
+ import { environmentVariableIsPopulated , getCustom , invokeCommand } from "../tools.js" ;
7
5
8
6
function getPipFreezeOutput ( ) {
9
- return environmentVariableIsPopulated ( "EXHORT_PIP_FREEZE" ) ? new Buffer ( process . env [ "EXHORT_PIP_FREEZE" ] , 'base64' ) . toString ( 'ascii' ) : execSync ( ` ${ handleSpacesInPath ( this . pathToPipBin ) } freeze --all` , err => {
10
- if ( err ) {
11
- throw new Error ( 'fail invoking pip freeze to fetch all installed dependencies in environment --> ' + err . message )
12
- }
13
- } ) . toString ( ) ;
7
+ try {
8
+ return environmentVariableIsPopulated ( "EXHORT_PIP_FREEZE" ) ? new Buffer . from ( process . env [ "EXHORT_PIP_FREEZE" ] , 'base64' ) . toString ( 'ascii' ) : invokeCommand ( this . pathToPipBin , [ 'freeze' , '--all' ] ) . toString ( ) ;
9
+ } catch ( error ) {
10
+ throw new Error ( 'Failed invoking \'pip freeze\' to list all installed packages in environment' , { cause : error } )
11
+ }
14
12
}
15
13
16
14
function getPipShowOutput ( depNames ) {
17
-
18
- return environmentVariableIsPopulated ( "EXHORT_PIP_SHOW" ) ? new Buffer ( process . env [ "EXHORT_PIP_SHOW" ] , 'base64' ) . toString ( 'ascii' ) : execSync ( `${ handleSpacesInPath ( this . pathToPipBin ) } show ${ depNames } ` , err => {
19
- if ( err ) {
20
- throw new Error ( 'fail invoking pip show to fetch all installed dependencies metadata --> ' + err . message )
21
- }
22
- } ) . toString ( ) ;
15
+ try {
16
+ return environmentVariableIsPopulated ( "EXHORT_PIP_SHOW" ) ? new Buffer . from ( process . env [ "EXHORT_PIP_SHOW" ] , 'base64' ) . toString ( 'ascii' ) : invokeCommand ( this . pathToPipBin , [ 'show' , ...depNames ] ) . toString ( ) ;
17
+ } catch ( error ) {
18
+ throw new Error ( 'fail invoking \'pip show\' to fetch metadata for all installed packages in environment' , { cause : error } )
19
+ }
23
20
}
24
21
25
22
/** @typedef {{name: string, version: string, dependencies: DependencyEntry[]} } DependencyEntry */
26
23
27
-
28
-
29
24
export default class Python_controller {
30
25
31
26
pythonEnvDir
@@ -51,26 +46,23 @@ export default class Python_controller {
51
46
this . pathToRequirements = pathToRequirements
52
47
this . options = options
53
48
}
54
- prepareEnvironment ( )
55
- {
49
+ prepareEnvironment ( ) {
56
50
if ( ! this . realEnvironment ) {
57
- this . pythonEnvDir = path . join ( path . sep , "tmp" , "exhort_env_js" )
58
- execSync ( `${ handleSpacesInPath ( this . pathToPythonBin ) } -m venv ${ handleSpacesInPath ( this . pythonEnvDir ) } ` , err => {
59
- if ( err ) {
60
- throw new Error ( 'failed creating virtual python environment - ' + err . message )
61
- }
62
- } )
63
- if ( this . pathToPythonBin . includes ( "python3" ) )
64
- {
51
+ this . pythonEnvDir = path . join ( path . sep , "tmp" , "exhort_env_js" )
52
+ try {
53
+ invokeCommand ( this . pathToPythonBin , [ '-m' , 'venv' , this . pythonEnvDir ] )
54
+ } catch ( error ) {
55
+ throw new Error ( 'Failed creating virtual python environment' , { cause : error } )
56
+ }
57
+ if ( this . pathToPythonBin . includes ( "python3" ) ) {
65
58
this . pathToPipBin = path . join ( path . sep , this . pythonEnvDir , os . platform ( ) === 'win32' ? "Scripts" : "bin" , this . #decideIfWindowsOrLinuxPath( "pip3" ) )
66
59
this . pathToPythonBin = path . join ( path . sep , this . pythonEnvDir , os . platform ( ) === 'win32' ? "Scripts" : "bin" , this . #decideIfWindowsOrLinuxPath( "python3" ) )
67
60
if ( os . platform ( ) === 'win32' ) {
68
61
let driveLetter = path . parse ( process . cwd ( ) ) . root
69
62
this . pathToPythonBin = `${ driveLetter } ${ this . pathToPythonBin . substring ( 1 ) } `
70
63
this . pathToPipBin = `${ driveLetter } ${ this . pathToPipBin . substring ( 1 ) } `
71
64
}
72
- }
73
- else {
65
+ } else {
74
66
this . pathToPipBin = path . join ( path . sep , this . pythonEnvDir , os . platform ( ) === 'win32' ? "Scripts" : "bin" , this . #decideIfWindowsOrLinuxPath( "pip" ) ) ;
75
67
this . pathToPythonBin = path . join ( path . sep , this . pythonEnvDir , os . platform ( ) === 'win32' ? "Scripts" : "bin" , this . #decideIfWindowsOrLinuxPath( "python" ) )
76
68
if ( os . platform ( ) === 'win32' ) {
@@ -80,18 +72,15 @@ export default class Python_controller {
80
72
}
81
73
}
82
74
// upgrade pip version to latest
83
- execSync ( `${ handleSpacesInPath ( this . pathToPythonBin ) } -m pip install --upgrade pip ` , err => {
84
- if ( err ) {
85
- throw new Error ( 'failed upgrading pip version on virtual python environment - ' + err . message )
86
- }
87
- } )
88
- }
89
- else {
75
+ try {
76
+ invokeCommand ( this . pathToPythonBin , [ '-m' , 'pip' , 'install' , '--upgrade' , 'pip' ] )
77
+ } catch ( error ) {
78
+ throw new Error ( 'Failed upgrading pip version in virtual python environment' , { cause : error } )
79
+ }
80
+ } else {
90
81
if ( this . pathToPythonBin . startsWith ( "python" ) ) {
91
82
this . pythonEnvDir = process . cwd ( )
92
- }
93
- else
94
- {
83
+ } else {
95
84
this . pythonEnvDir = path . dirname ( this . pathToPythonBin )
96
85
}
97
86
}
@@ -100,8 +89,7 @@ export default class Python_controller {
100
89
#decideIfWindowsOrLinuxPath( fileName ) {
101
90
if ( os . platform ( ) === "win32" ) {
102
91
return fileName + ".exe"
103
- }
104
- else {
92
+ } else {
105
93
return fileName
106
94
}
107
95
}
@@ -110,8 +98,7 @@ export default class Python_controller {
110
98
* @param {boolean } includeTransitive - whether to return include in returned object transitive dependencies or not
111
99
* @return {[DependencyEntry] }
112
100
*/
113
- getDependencies ( includeTransitive )
114
- {
101
+ getDependencies ( includeTransitive ) {
115
102
let startingTime
116
103
let endingTime
117
104
if ( process . env [ "EXHORT_DEBUG" ] === "true" ) {
@@ -120,21 +107,19 @@ export default class Python_controller {
120
107
}
121
108
if ( ! this . realEnvironment ) {
122
109
let installBestEfforts = getCustom ( "EXHORT_PYTHON_INSTALL_BEST_EFFORTS" , "false" , this . options ) ;
123
- if ( installBestEfforts === "false" )
124
- {
125
- execSync ( `${ handleSpacesInPath ( this . pathToPipBin ) } install -r ${ handleSpacesInPath ( this . pathToRequirements ) } ` , err => {
126
- if ( err ) {
127
- throw new Error ( 'fail installing requirements.txt manifest in created virtual python environment --> ' + err . message )
128
- }
129
- } )
110
+ if ( installBestEfforts === "false" ) {
111
+ try {
112
+ invokeCommand ( this . pathToPipBin , [ 'install' , '-r' , this . pathToRequirements ] )
113
+ } catch ( error ) {
114
+ throw new Error ( 'Failed installing requirements.txt manifest in virtual python environment' , { cause : error } )
115
+ }
130
116
}
131
117
// make best efforts to install the requirements.txt on the virtual environment created from the python3 passed in.
132
118
// that means that it will install the packages without referring to the versions, but will let pip choose the version
133
119
// tailored for version of the python environment( and of pip package manager) for each package.
134
120
else {
135
121
let matchManifestVersions = getCustom ( "MATCH_MANIFEST_VERSIONS" , "true" , this . options ) ;
136
- if ( matchManifestVersions === "true" )
137
- {
122
+ if ( matchManifestVersions === "true" ) {
138
123
throw new Error ( "Conflicting settings, EXHORT_PYTHON_INSTALL_BEST_EFFORTS=true can only work with MATCH_MANIFEST_VERSIONS=false" )
139
124
}
140
125
this . #installingRequirementsOneByOne( )
@@ -156,27 +141,26 @@ export default class Python_controller {
156
141
let requirementsRows = requirementsContent . toString ( ) . split ( EOL ) ;
157
142
requirementsRows . filter ( ( line ) => ! line . trim ( ) . startsWith ( "#" ) ) . filter ( ( line ) => line . trim ( ) !== "" ) . forEach ( ( dependency ) => {
158
143
let dependencyName = getDependencyName ( dependency ) ;
159
- execSync ( ` ${ handleSpacesInPath ( this . pathToPipBin ) } install ${ dependencyName } ` , err => {
160
- if ( err ) {
161
- throw new Error ( `Best efforts process - failed installing ${ dependencyName } in created virtual python environment --> error message: ` + err . message )
162
- }
163
- } )
164
- } )
144
+ try {
145
+ invokeCommand ( this . pathToPipBin , [ 'install' , dependencyName ] )
146
+ } catch ( error ) {
147
+ throw new Error ( `Failed in best-effort installing ${ dependencyName } in virtual python environment` , { cause : error } )
148
+ }
149
+ } )
165
150
}
166
151
/**
167
152
* @private
168
153
*/
169
- #cleanEnvironment( )
170
- {
171
- if ( ! this . realEnvironment )
172
- {
173
- execSync ( `${ handleSpacesInPath ( this . pathToPipBin ) } uninstall -y -r ${ handleSpacesInPath ( this . pathToRequirements ) } ` , err => {
174
- if ( err ) {
175
- throw new Error ( 'fail uninstalling requirements.txt in created virtual python environment --> ' + err . message )
176
- }
177
- } )
154
+ #cleanEnvironment( ) {
155
+ if ( ! this . realEnvironment ) {
156
+ try {
157
+ invokeCommand ( this . pathToPipBin , [ 'uninstall' , '-y' , '-r' , this . pathToRequirements ] )
158
+ } catch ( error ) {
159
+ throw new Error ( 'Failed uninstalling requirements.txt in virtual python environment' , { cause : error } )
160
+ }
178
161
}
179
162
}
163
+
180
164
#getDependenciesImpl( includeTransitive ) {
181
165
let dependencies = new Array ( )
182
166
let usePipDepTree = getCustom ( "EXHORT_PIP_USE_DEP_TREE" , "false" , this . options ) ;
@@ -189,7 +173,7 @@ export default class Python_controller {
189
173
if ( usePipDepTree !== "true" ) {
190
174
freezeOutput = getPipFreezeOutput . call ( this ) ;
191
175
lines = freezeOutput . split ( EOL )
192
- depNames = lines . map ( line => getDependencyName ( line ) ) . join ( " " )
176
+ depNames = lines . map ( line => getDependencyName ( line ) )
193
177
}
194
178
else {
195
179
pipDepTreeJsonArrayOutput = getDependencyTreeJsonFromPipDepTree ( this . pathToPipBin , this . pathToPythonBin )
@@ -198,7 +182,7 @@ export default class Python_controller {
198
182
199
183
if ( usePipDepTree !== "true" ) {
200
184
pipShowOutput = getPipShowOutput . call ( this , depNames ) ;
201
- allPipShowDeps = pipShowOutput . split ( EOL + "---" + EOL ) ;
185
+ allPipShowDeps = pipShowOutput . split ( EOL + "---" + EOL ) ;
202
186
}
203
187
//debug
204
188
// pipShowOutput = "alternative pip show output goes here for debugging"
@@ -213,8 +197,7 @@ export default class Python_controller {
213
197
CachedEnvironmentDeps [ dependencyName . replace ( "-" , "_" ) ] = record
214
198
CachedEnvironmentDeps [ dependencyName . replace ( "_" , "-" ) ] = record
215
199
} )
216
- }
217
- else {
200
+ } else {
218
201
pipDepTreeJsonArrayOutput . forEach ( depTreeEntry => {
219
202
let packageName = depTreeEntry [ "package" ] [ "package_name" ] . toLowerCase ( )
220
203
let pipDepTreeEntryForCache = {
@@ -229,18 +212,15 @@ export default class Python_controller {
229
212
}
230
213
linesOfRequirements . forEach ( ( dep ) => {
231
214
// if matchManifestVersions setting is turned on , then
232
- if ( matchManifestVersions === "true" )
233
- {
215
+ if ( matchManifestVersions === "true" ) {
234
216
let dependencyName
235
217
let manifestVersion
236
218
let installedVersion
237
219
let doubleEqualSignPosition
238
- if ( dep . includes ( "==" ) )
239
- {
220
+ if ( dep . includes ( "==" ) ) {
240
221
doubleEqualSignPosition = dep . indexOf ( "==" )
241
222
manifestVersion = dep . substring ( doubleEqualSignPosition + 2 ) . trim ( )
242
- if ( manifestVersion . includes ( "#" ) )
243
- {
223
+ if ( manifestVersion . includes ( "#" ) ) {
244
224
let hashCharIndex = manifestVersion . indexOf ( "#" ) ;
245
225
manifestVersion = manifestVersion . substring ( 0 , hashCharIndex )
246
226
}
@@ -249,17 +229,15 @@ export default class Python_controller {
249
229
if ( CachedEnvironmentDeps [ dependencyName . toLowerCase ( ) ] !== undefined ) {
250
230
if ( usePipDepTree !== "true" ) {
251
231
installedVersion = getDependencyVersion ( CachedEnvironmentDeps [ dependencyName . toLowerCase ( ) ] )
252
- }
253
- else {
232
+ } else {
254
233
installedVersion = CachedEnvironmentDeps [ dependencyName . toLowerCase ( ) ] . version
255
234
}
256
235
}
257
236
if ( installedVersion ) {
258
237
if ( manifestVersion . trim ( ) !== installedVersion . trim ( ) ) {
259
- throw new Error ( `Can't continue with analysis - versions mismatch for dependency name ${ dependencyName } , manifest version=${ manifestVersion } , installed Version =${ installedVersion } , if you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting - MATCH_MANIFEST_VERSIONS=false` )
238
+ throw new Error ( `Can't continue with analysis - versions mismatch for dependency name ${ dependencyName } ( manifest version=${ manifestVersion } , installed version =${ installedVersion } ).If you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting MATCH_MANIFEST_VERSIONS=false` )
260
239
}
261
240
}
262
-
263
241
}
264
242
}
265
243
let path = new Array ( )
@@ -274,11 +252,11 @@ export default class Python_controller {
274
252
if ( DEP1 < DEP2 ) {
275
253
return - 1 ;
276
254
}
277
- if ( DEP1 > DEP2 )
278
- {
255
+ if ( DEP1 > DEP2 ) {
279
256
return 1 ;
280
257
}
281
- return 0 ; } )
258
+ return 0 ;
259
+ } )
282
260
return dependencies
283
261
}
284
262
}
@@ -321,8 +299,7 @@ function getDependencyName(depLine) {
321
299
const regex = / [ \w \s - _ . ] + / g;
322
300
if ( depLine . match ( regex ) ) {
323
301
result = depLine . match ( regex ) [ 0 ]
324
- }
325
- else {
302
+ } else {
326
303
result = depLine
327
304
}
328
305
}
@@ -358,10 +335,7 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
358
335
}
359
336
let record = cachedEnvironmentDeps [ dependencyName . toLowerCase ( ) ]
360
337
if ( record === null || record === undefined ) {
361
- throw new Error ( `Package name=>${ dependencyName } is not installed in your python environment,
362
- either install it ( better to install requirements.txt altogether) or set
363
- the setting EXHORT_PYTHON_VIRTUAL_ENV to true to automatically install
364
- it in virtual environment (please note that this may slow down the analysis) ` )
338
+ throw new Error ( `Package ${ dependencyName } is not installed in your python environment, either install it (better to install requirements.txt altogether) or set the setting EXHORT_PYTHON_VIRTUAL_ENV=true to automatically install it in virtual environment (please note that this may slow down the analysis)` )
365
339
}
366
340
let depName
367
341
let version ;
@@ -370,8 +344,7 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
370
344
depName = getDependencyNameShow ( record )
371
345
version = getDependencyVersion ( record ) ;
372
346
directDeps = getDepsList ( record )
373
- }
374
- else {
347
+ } else {
375
348
depName = record . name
376
349
version = record . version
377
350
directDeps = record . dependencies
@@ -398,11 +371,11 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
398
371
if ( DEP1 < DEP2 ) {
399
372
return - 1 ;
400
373
}
401
- if ( DEP1 > DEP2 )
402
- {
374
+ if ( DEP1 > DEP2 ) {
403
375
return 1 ;
404
376
}
405
- return 0 ; } )
377
+ return 0 ;
378
+ } )
406
379
407
380
entry [ "dependencies" ] = targetDeps
408
381
} )
@@ -418,20 +391,19 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
418
391
function getDependencyTreeJsonFromPipDepTree ( pipPath , pythonPath ) {
419
392
let dependencyTree
420
393
try {
421
- execSync ( ` ${ handleSpacesInPath ( pipPath ) } install pipdeptree` )
422
- } catch ( e ) {
423
- throw new Error ( `Couldn't install pipdeptree utility, reason: ${ e . getMessage } ` )
394
+ invokeCommand ( pipPath , [ ' install' , ' pipdeptree' ] )
395
+ } catch ( error ) {
396
+ throw new Error ( `Failed installing pipdeptree utility` , { cause : error } )
424
397
}
425
398
426
399
try {
427
400
if ( pythonPath . startsWith ( "python" ) ) {
428
- dependencyTree = execSync ( `pipdeptree --json` ) . toString ( )
429
- }
430
- else {
431
- dependencyTree = execSync ( `pipdeptree --json --python ${ handleSpacesInPath ( pythonPath ) } ` ) . toString ( )
401
+ dependencyTree = invokeCommand ( 'pipdeptree' , [ '--json' ] ) . toString ( )
402
+ } else {
403
+ dependencyTree = invokeCommand ( 'pipdeptree' , [ '--json' , '--python' , pythonPath ] ) . toString ( )
432
404
}
433
- } catch ( e ) {
434
- throw new Error ( `couldn't produce dependency tree using pipdeptree tool, stop analysis, message -> ${ e . getMessage } ` )
405
+ } catch ( error ) {
406
+ throw new Error ( `Failed building dependency tree using pipdeptree tool, stopping analysis` , { cause : error } )
435
407
}
436
408
437
409
return JSON . parse ( dependencyTree )
0 commit comments