1- import { execSync } from "node:child_process" ;
21import fs from "node:fs" ;
32import path from 'node:path' ;
43import os , { EOL } from "os" ;
5- import { environmentVariableIsPopulated , getCustom , handleSpacesInPath } from "../tools.js" ;
6-
4+ import { environmentVariableIsPopulated , getCustom , invokeCommand } from "../tools.js" ;
75
86function 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+ }
1412}
1513
1614function 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+ }
2320}
2421
2522/** @typedef {{name: string, version: string, dependencies: DependencyEntry[]} } DependencyEntry */
2623
27-
28-
2924export default class Python_controller {
3025
3126 pythonEnvDir
@@ -51,26 +46,23 @@ export default class Python_controller {
5146 this . pathToRequirements = pathToRequirements
5247 this . options = options
5348 }
54- prepareEnvironment ( )
55- {
49+ prepareEnvironment ( ) {
5650 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" ) ) {
6558 this . pathToPipBin = path . join ( path . sep , this . pythonEnvDir , os . platform ( ) === 'win32' ? "Scripts" : "bin" , this . #decideIfWindowsOrLinuxPath( "pip3" ) )
6659 this . pathToPythonBin = path . join ( path . sep , this . pythonEnvDir , os . platform ( ) === 'win32' ? "Scripts" : "bin" , this . #decideIfWindowsOrLinuxPath( "python3" ) )
6760 if ( os . platform ( ) === 'win32' ) {
6861 let driveLetter = path . parse ( process . cwd ( ) ) . root
6962 this . pathToPythonBin = `${ driveLetter } ${ this . pathToPythonBin . substring ( 1 ) } `
7063 this . pathToPipBin = `${ driveLetter } ${ this . pathToPipBin . substring ( 1 ) } `
7164 }
72- }
73- else {
65+ } else {
7466 this . pathToPipBin = path . join ( path . sep , this . pythonEnvDir , os . platform ( ) === 'win32' ? "Scripts" : "bin" , this . #decideIfWindowsOrLinuxPath( "pip" ) ) ;
7567 this . pathToPythonBin = path . join ( path . sep , this . pythonEnvDir , os . platform ( ) === 'win32' ? "Scripts" : "bin" , this . #decideIfWindowsOrLinuxPath( "python" ) )
7668 if ( os . platform ( ) === 'win32' ) {
@@ -80,18 +72,15 @@ export default class Python_controller {
8072 }
8173 }
8274 // 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 {
9081 if ( this . pathToPythonBin . startsWith ( "python" ) ) {
9182 this . pythonEnvDir = process . cwd ( )
92- }
93- else
94- {
83+ } else {
9584 this . pythonEnvDir = path . dirname ( this . pathToPythonBin )
9685 }
9786 }
@@ -100,8 +89,7 @@ export default class Python_controller {
10089 #decideIfWindowsOrLinuxPath( fileName ) {
10190 if ( os . platform ( ) === "win32" ) {
10291 return fileName + ".exe"
103- }
104- else {
92+ } else {
10593 return fileName
10694 }
10795 }
@@ -110,8 +98,7 @@ export default class Python_controller {
11098 * @param {boolean } includeTransitive - whether to return include in returned object transitive dependencies or not
11199 * @return {[DependencyEntry] }
112100 */
113- getDependencies ( includeTransitive )
114- {
101+ getDependencies ( includeTransitive ) {
115102 let startingTime
116103 let endingTime
117104 if ( process . env [ "EXHORT_DEBUG" ] === "true" ) {
@@ -120,21 +107,19 @@ export default class Python_controller {
120107 }
121108 if ( ! this . realEnvironment ) {
122109 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+ }
130116 }
131117 // make best efforts to install the requirements.txt on the virtual environment created from the python3 passed in.
132118 // that means that it will install the packages without referring to the versions, but will let pip choose the version
133119 // tailored for version of the python environment( and of pip package manager) for each package.
134120 else {
135121 let matchManifestVersions = getCustom ( "MATCH_MANIFEST_VERSIONS" , "true" , this . options ) ;
136- if ( matchManifestVersions === "true" )
137- {
122+ if ( matchManifestVersions === "true" ) {
138123 throw new Error ( "Conflicting settings, EXHORT_PYTHON_INSTALL_BEST_EFFORTS=true can only work with MATCH_MANIFEST_VERSIONS=false" )
139124 }
140125 this . #installingRequirementsOneByOne( )
@@ -156,27 +141,26 @@ export default class Python_controller {
156141 let requirementsRows = requirementsContent . toString ( ) . split ( EOL ) ;
157142 requirementsRows . filter ( ( line ) => ! line . trim ( ) . startsWith ( "#" ) ) . filter ( ( line ) => line . trim ( ) !== "" ) . forEach ( ( dependency ) => {
158143 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+ } )
165150 }
166151 /**
167152 * @private
168153 */
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+ }
178161 }
179162 }
163+
180164 #getDependenciesImpl( includeTransitive ) {
181165 let dependencies = new Array ( )
182166 let usePipDepTree = getCustom ( "EXHORT_PIP_USE_DEP_TREE" , "false" , this . options ) ;
@@ -189,7 +173,7 @@ export default class Python_controller {
189173 if ( usePipDepTree !== "true" ) {
190174 freezeOutput = getPipFreezeOutput . call ( this ) ;
191175 lines = freezeOutput . split ( EOL )
192- depNames = lines . map ( line => getDependencyName ( line ) ) . join ( " " )
176+ depNames = lines . map ( line => getDependencyName ( line ) )
193177 }
194178 else {
195179 pipDepTreeJsonArrayOutput = getDependencyTreeJsonFromPipDepTree ( this . pathToPipBin , this . pathToPythonBin )
@@ -198,7 +182,7 @@ export default class Python_controller {
198182
199183 if ( usePipDepTree !== "true" ) {
200184 pipShowOutput = getPipShowOutput . call ( this , depNames ) ;
201- allPipShowDeps = pipShowOutput . split ( EOL + "---" + EOL ) ;
185+ allPipShowDeps = pipShowOutput . split ( EOL + "---" + EOL ) ;
202186 }
203187 //debug
204188 // pipShowOutput = "alternative pip show output goes here for debugging"
@@ -213,8 +197,7 @@ export default class Python_controller {
213197 CachedEnvironmentDeps [ dependencyName . replace ( "-" , "_" ) ] = record
214198 CachedEnvironmentDeps [ dependencyName . replace ( "_" , "-" ) ] = record
215199 } )
216- }
217- else {
200+ } else {
218201 pipDepTreeJsonArrayOutput . forEach ( depTreeEntry => {
219202 let packageName = depTreeEntry [ "package" ] [ "package_name" ] . toLowerCase ( )
220203 let pipDepTreeEntryForCache = {
@@ -229,18 +212,15 @@ export default class Python_controller {
229212 }
230213 linesOfRequirements . forEach ( ( dep ) => {
231214 // if matchManifestVersions setting is turned on , then
232- if ( matchManifestVersions === "true" )
233- {
215+ if ( matchManifestVersions === "true" ) {
234216 let dependencyName
235217 let manifestVersion
236218 let installedVersion
237219 let doubleEqualSignPosition
238- if ( dep . includes ( "==" ) )
239- {
220+ if ( dep . includes ( "==" ) ) {
240221 doubleEqualSignPosition = dep . indexOf ( "==" )
241222 manifestVersion = dep . substring ( doubleEqualSignPosition + 2 ) . trim ( )
242- if ( manifestVersion . includes ( "#" ) )
243- {
223+ if ( manifestVersion . includes ( "#" ) ) {
244224 let hashCharIndex = manifestVersion . indexOf ( "#" ) ;
245225 manifestVersion = manifestVersion . substring ( 0 , hashCharIndex )
246226 }
@@ -249,17 +229,15 @@ export default class Python_controller {
249229 if ( CachedEnvironmentDeps [ dependencyName . toLowerCase ( ) ] !== undefined ) {
250230 if ( usePipDepTree !== "true" ) {
251231 installedVersion = getDependencyVersion ( CachedEnvironmentDeps [ dependencyName . toLowerCase ( ) ] )
252- }
253- else {
232+ } else {
254233 installedVersion = CachedEnvironmentDeps [ dependencyName . toLowerCase ( ) ] . version
255234 }
256235 }
257236 if ( installedVersion ) {
258237 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` )
260239 }
261240 }
262-
263241 }
264242 }
265243 let path = new Array ( )
@@ -274,11 +252,11 @@ export default class Python_controller {
274252 if ( DEP1 < DEP2 ) {
275253 return - 1 ;
276254 }
277- if ( DEP1 > DEP2 )
278- {
255+ if ( DEP1 > DEP2 ) {
279256 return 1 ;
280257 }
281- return 0 ; } )
258+ return 0 ;
259+ } )
282260 return dependencies
283261 }
284262}
@@ -321,8 +299,7 @@ function getDependencyName(depLine) {
321299 const regex = / [ \w \s - _ . ] + / g;
322300 if ( depLine . match ( regex ) ) {
323301 result = depLine . match ( regex ) [ 0 ]
324- }
325- else {
302+ } else {
326303 result = depLine
327304 }
328305 }
@@ -358,10 +335,7 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
358335 }
359336 let record = cachedEnvironmentDeps [ dependencyName . toLowerCase ( ) ]
360337 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)` )
365339 }
366340 let depName
367341 let version ;
@@ -370,8 +344,7 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
370344 depName = getDependencyNameShow ( record )
371345 version = getDependencyVersion ( record ) ;
372346 directDeps = getDepsList ( record )
373- }
374- else {
347+ } else {
375348 depName = record . name
376349 version = record . version
377350 directDeps = record . dependencies
@@ -398,11 +371,11 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
398371 if ( DEP1 < DEP2 ) {
399372 return - 1 ;
400373 }
401- if ( DEP1 > DEP2 )
402- {
374+ if ( DEP1 > DEP2 ) {
403375 return 1 ;
404376 }
405- return 0 ; } )
377+ return 0 ;
378+ } )
406379
407380 entry [ "dependencies" ] = targetDeps
408381 } )
@@ -418,20 +391,19 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
418391function getDependencyTreeJsonFromPipDepTree ( pipPath , pythonPath ) {
419392 let dependencyTree
420393 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 } )
424397 }
425398
426399 try {
427400 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 ( )
432404 }
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 } )
435407 }
436408
437409 return JSON . parse ( dependencyTree )
0 commit comments