77import java .nio .file .Files ;
88import java .nio .file .StandardCopyOption ;
99import java .util .Arrays ;
10-
10+ import java .util .Collections ;
11+ import java .util .HashMap ;
12+ import java .util .LinkedList ;
13+ import java .util .List ;
14+ import java .util .stream .Stream ;
15+
16+ import com .fasterxml .jackson .databind .ObjectMapper ;
17+ import com .vdurmont .semver4j .Requirement ;
18+ import com .vdurmont .semver4j .Semver ;
1119import org .apache .commons .io .FileUtils ;
1220import org .slf4j .Logger ;
1321import org .slf4j .LoggerFactory ;
@@ -28,6 +36,8 @@ public class NodeInstaller {
2836
2937 private final FileDownloader fileDownloader ;
3038
39+ private Requirement nodeVersionRequirement ;
40+
3141 NodeInstaller (InstallConfig config , ArchiveExtractor archiveExtractor , FileDownloader fileDownloader ) {
3242 this .logger = LoggerFactory .getLogger (getClass ());
3343 this .config = config ;
@@ -74,13 +84,65 @@ private boolean npmProvided() throws InstallationException {
7484 return false ;
7585 }
7686
77- public void install () throws InstallationException {
87+ public String install () throws InstallationException {
7888 // use static lock object for a synchronized block
7989 synchronized (LOCK ) {
8090 if (this .nodeDownloadRoot == null || this .nodeDownloadRoot .isEmpty ()) {
8191 this .nodeDownloadRoot = this .config .getPlatform ().getNodeDownloadRoot ();
8292 }
93+
94+ if ("engines" .equals (this .nodeVersion )) {
95+ try {
96+ File packageFile = new File (this .config .getWorkingDirectory (), "package.json" );
97+ HashMap <String , Object > data = new ObjectMapper ().readValue (packageFile , HashMap .class );
98+ if (data .containsKey ("engines" )) {
99+ HashMap <String , Object > engines = (HashMap <String , Object >) data .get ("engines" );
100+ if (engines .containsKey ("node" )) {
101+ this .nodeVersionRequirement = Requirement .buildNPM ((String ) engines .get ("node" ));
102+ } else {
103+ this .logger .info ("Could not read node from engines from package.json" );
104+ }
105+ } else {
106+ this .logger .info ("Could not read engines from package.json" );
107+ }
108+ } catch (IOException e ) {
109+ throw new InstallationException ("Could not read node engine version from package.json" , e );
110+ }
111+ }
112+
83113 if (!nodeIsAlreadyInstalled ()) {
114+ if (this .nodeVersionRequirement != null ) {
115+ // download available node versions
116+ try {
117+ String downloadUrl = this .nodeDownloadRoot
118+ + "index.json" ;
119+
120+ File tmpDirectory = getTempDirectory ();
121+
122+ File archive = File .createTempFile ("node_versions" , ".json" , tmpDirectory );
123+
124+ downloadFile (downloadUrl , archive , this .userName , this .password );
125+
126+ HashMap <String , Object >[] data = new ObjectMapper ().readValue (archive , HashMap [].class );
127+
128+ List <String > nodeVersions = new LinkedList <>();
129+ for (HashMap <String , Object > d : data ) {
130+ if (d .containsKey ("version" )) {
131+ nodeVersions .add ((String ) d .get ("version" ));
132+ }
133+ }
134+
135+ // we want the oldest possible version, that satisfies the requirements
136+ Collections .reverse (nodeVersions );
137+
138+ logger .debug ("Available node versions: {}" , nodeVersions );
139+ this .nodeVersion = nodeVersions .stream ().filter (version -> nodeVersionRequirement .isSatisfiedBy (new Semver (version , Semver .SemverType .NPM ))).findFirst ().orElseThrow (() -> new InstallationException ("Could not find matching node version satisfying requirement " + this .nodeVersionRequirement ));
140+ this .logger .info ("Found matching node version {} satisfying requirement {}." , this .nodeVersion , this .nodeVersionRequirement );
141+ } catch (IOException | DownloadException e ) {
142+ throw new InstallationException ("Could not get available node versions." , e );
143+ }
144+ }
145+
84146 this .logger .info ("Installing node version {}" , this .nodeVersion );
85147 if (!this .nodeVersion .startsWith ("v" )) {
86148 this .logger .warn ("Node version does not start with naming convention 'v'." );
@@ -96,6 +158,8 @@ public void install() throws InstallationException {
96158 }
97159 }
98160 }
161+
162+ return nodeVersion ;
99163 }
100164
101165 private boolean nodeIsAlreadyInstalled () {
@@ -104,14 +168,19 @@ private boolean nodeIsAlreadyInstalled() {
104168 File nodeFile = executorConfig .getNodePath ();
105169 if (nodeFile .exists ()) {
106170 final String version =
107- new NodeExecutor (executorConfig , Arrays .asList ("--version" ), null ).executeAndGetResult (logger );
171+ new NodeExecutor (executorConfig , Arrays .asList ("--version" ), null ).executeAndGetResult (logger );
108172
109- if (version .equals (this .nodeVersion )) {
173+ if (nodeVersionRequirement != null && nodeVersionRequirement .isSatisfiedBy (new Semver (version , Semver .SemverType .NPM ))) {
174+ //update version with installed version
175+ this .nodeVersion = version ;
176+ this .logger .info ("Node {} matches required version range {} installed." , version , nodeVersionRequirement );
177+ return true ;
178+ } else if (version .equals (this .nodeVersion )) {
110179 this .logger .info ("Node {} is already installed." , version );
111180 return true ;
112181 } else {
113182 this .logger .info ("Node {} was installed, but we need version {}" , version ,
114- this .nodeVersion );
183+ this .nodeVersion );
115184 return false ;
116185 }
117186 } else {
0 commit comments