From 502f38938f1e8cb6b736b5b2a5f8c08c3772e033 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Mon, 30 Oct 2017 11:10:08 -0700 Subject: [PATCH] Add support for specifying NodeJS as a required toolset --- README.md | 7 ++ modules/KoreBuild.Tasks/GetToolsets.cs | 107 +++++++++++++++++- .../Utilities/EnvironmentHelper.cs | 38 +++++++ modules/KoreBuild.Tasks/module.targets | 1 + .../KoreBuildSettingsTest.cs | 33 ++++++ tools/KoreBuildSettings.cs | 22 +++- tools/korebuild.schema.json | 24 +++- 7 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 modules/KoreBuild.Tasks/Utilities/EnvironmentHelper.cs diff --git a/README.md b/README.md index e5af50d..5c71e29 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ KoreBuild can be configured by adding a 'korebuild.json' file into the root fold Example: ```js +// NB: Don't actually use comments in JSON files. PowerShell's ConvertFrom-Json will throw an error. + { // add this for editor auto-completion :) "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", @@ -66,6 +68,11 @@ Example: // This tool is only required on Windows. "required": [ "windows" ] + }, + + "nodejs": { + "required": true, + "minVersion": "8.0" } } } diff --git a/modules/KoreBuild.Tasks/GetToolsets.cs b/modules/KoreBuild.Tasks/GetToolsets.cs index 60583f7..54886d5 100644 --- a/modules/KoreBuild.Tasks/GetToolsets.cs +++ b/modules/KoreBuild.Tasks/GetToolsets.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using KoreBuild.Tasks.Utilities; @@ -20,17 +22,26 @@ namespace KoreBuild.Tasks public string ConfigFile { get; set; } /// - /// The path to MSBuild.exe (x86), if the 'visualstudio' toolset was specified. It will be empty if this file does not exist. + /// The path to MSBuild.exe (x86) if the 'visualstudio' toolset was specified in korebuild.json. + /// It will be empty if not specified or if this file does not exist. /// [Output] public string VisualStudioMSBuildx86Path { get; set; } /// - /// The path to MSBuild.exe (x64), if the 'visualstudio' toolset was specified. It will be empty if this file does not exist. + /// The path to MSBuild.exe (x64) if the 'visualstudio' toolset was specified in korebuild.json. + /// It will be empty if not specified or if this file does not exist. /// [Output] public string VisualStudioMSBuildx64Path { get; set; } + /// + /// The path to NodeJS.exe if the 'nodejs' toolset was specified in korebuild.json. + /// It will be empty if not specified. + /// + [Output] + public string NodeJSPath { get; set; } + public override bool Execute() { if (!File.Exists(ConfigFile)) @@ -54,6 +65,9 @@ namespace KoreBuild.Tasks case KoreBuildSettings.VisualStudioToolset vs: GetVisualStudio(vs); break; + case KoreBuildSettings.NodeJSToolset node: + GetNode(node); + break; default: Log.LogWarning("Toolset checks not implemented for " + toolset.GetType().Name); break; @@ -93,5 +107,94 @@ namespace KoreBuild.Tasks VisualStudioMSBuildx86Path = vs.GetMSBuildx86SubPath(); VisualStudioMSBuildx64Path = vs.GetMSBuildx64SubPath(); } + + private void GetNode(KoreBuildSettings.NodeJSToolset nodeToolset) + { + var nodePath = EnvironmentHelper.GetCommandOnPath("nodejs") ?? EnvironmentHelper.GetCommandOnPath("node"); + + var required = IsRequiredOnThisPlatform(nodeToolset.Required); + + if (string.IsNullOrEmpty(nodePath)) + { + LogFailure( + isError: required, + message: $"Could not find NodeJS on PATH."); + return; + } + + Log.LogMessage(MessageImportance.Low, "Found NodeJS in " + nodePath); + + if (nodeToolset.MinVersion == null) + { + NodeJSPath = nodePath; + return; + } + + var process = Process.Start(new ProcessStartInfo + { + FileName = nodePath, + Arguments = "--version", + RedirectStandardOutput = true, + }); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + LogFailure( + isError: required, + message: $"Found NodeJS in '{nodePath}', but could not determine the version of NodeJS installed. 'node --version' failed."); + return; + } + + var nodeVersionString = process.StandardOutput.ReadToEnd()?.Trim()?.TrimStart('v'); + if (!Version.TryParse(nodeVersionString, out var nodeVersion)) + { + LogFailure( + isError: required, + message: $"Found NodeJS in '{nodePath}', but could not determine the version of NodeJS installed. 'node --version' returned '{nodeVersionString}'."); + return; + } + + if (nodeVersion < nodeToolset.MinVersion) + { + LogFailure( + isError: required, + message: $"Found NodeJS in '{nodePath}', but its version '{nodeVersionString}' did not meet the required minimum version '{nodeToolset.MinVersion}' as specified in '{ConfigFile}'"); + return; + } + + Log.LogMessage(MessageImportance.High, "Using NodeJS {0} from {1}", nodeVersionString, nodePath); + NodeJSPath = nodePath; + } + + private void LogFailure(bool isError, string message) + { + if (isError) + { + Log.LogError(message); + } + else + { + Log.LogWarning(message); + } + } + + private bool IsRequiredOnThisPlatform(KoreBuildSettings.RequiredPlatforms platforms) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return (platforms & KoreBuildSettings.RequiredPlatforms.Windows) != 0; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return (platforms & KoreBuildSettings.RequiredPlatforms.Linux) != 0; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return (platforms & KoreBuildSettings.RequiredPlatforms.MacOS) != 0; + } + + return platforms != KoreBuildSettings.RequiredPlatforms.None; + } } } diff --git a/modules/KoreBuild.Tasks/Utilities/EnvironmentHelper.cs b/modules/KoreBuild.Tasks/Utilities/EnvironmentHelper.cs new file mode 100644 index 0000000..c0c55b3 --- /dev/null +++ b/modules/KoreBuild.Tasks/Utilities/EnvironmentHelper.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace KoreBuild.Tasks.Utilities +{ + internal class EnvironmentHelper + { + private static string[] _searchPaths; + private static string[] _executableExtensions; + + static EnvironmentHelper() + { + _searchPaths = Environment.GetEnvironmentVariable("PATH") + .Split(Path.PathSeparator) + .Select(p => p.Trim('"')) + .ToArray(); + + _executableExtensions = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Environment.GetEnvironmentVariable("PATHEXT").Split(';').Select(e => e.ToLower().Trim('"')).ToArray() + : Array.Empty(); + } + + public static string GetCommandOnPath(string exeName) + { + return _searchPaths.Join( + _executableExtensions, + p => true, + e => true, + (p, e) => Path.Combine(p, exeName + e)) + .FirstOrDefault(File.Exists); + } + } +} diff --git a/modules/KoreBuild.Tasks/module.targets b/modules/KoreBuild.Tasks/module.targets index cedab28..ff62edb 100644 --- a/modules/KoreBuild.Tasks/module.targets +++ b/modules/KoreBuild.Tasks/module.targets @@ -115,6 +115,7 @@ + diff --git a/test/KoreBuild.Tasks.Tests/KoreBuildSettingsTest.cs b/test/KoreBuild.Tasks.Tests/KoreBuildSettingsTest.cs index 2e7e9fe..ebae7bd 100644 --- a/test/KoreBuild.Tasks.Tests/KoreBuildSettingsTest.cs +++ b/test/KoreBuild.Tasks.Tests/KoreBuildSettingsTest.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using Newtonsoft.Json; using Xunit; namespace KoreBuild.Tasks.Tests @@ -96,5 +97,37 @@ namespace KoreBuild.Tasks.Tests var vs = Assert.IsType(toolset); Assert.Equal(platforms, vs.Required); } + + [Fact] + public void ItDeserializesNodeJSToolsetWithVersion() + { + File.WriteAllText(_configFile, @" +{ + ""toolsets"": { + ""nodejs"": { + ""minVersion"": ""8.0"" + } + } +}"); + var settings = KoreBuildSettings.Load(_configFile); + var toolset = Assert.Single(settings.Toolsets); + + var node = Assert.IsType(toolset); + Assert.Equal(new Version(8, 0), node.MinVersion); + } + + [Fact] + public void ItFailsIfVersionIsNotValid() + { + File.WriteAllText(_configFile, @" +{ + ""toolsets"": { + ""nodejs"": { + ""minVersion"": ""banana"" + } + } +}"); + Assert.Throws(() => KoreBuildSettings.Load(_configFile)); + } } } diff --git a/tools/KoreBuildSettings.cs b/tools/KoreBuildSettings.cs index 89139ce..201eccc 100644 --- a/tools/KoreBuildSettings.cs +++ b/tools/KoreBuildSettings.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; namespace KoreBuild @@ -42,6 +43,13 @@ namespace KoreBuild public string MinVersion { get; set; } public string[] RequiredWorkloads { get; set; } = Array.Empty(); } + + public class NodeJSToolset : KoreBuildToolset + { + [JsonConverter(typeof(VersionConverter))] + public Version MinVersion { get; set; } + } + public static KoreBuildSettings Load(string filePath) { using (var file = File.OpenText(filePath)) @@ -65,14 +73,22 @@ namespace KoreBuild foreach (var prop in obj.Properties()) { + KoreBuildToolset toolset; switch (prop.Name.ToLowerInvariant()) { case "visualstudio": - var vs = prop.Value.ToObject(); - toolsets.Add(vs); + toolset = prop.Value.ToObject(); + break; + case "nodejs": + toolset = prop.Value.ToObject(); break; default: - break; + continue; + } + + if (toolset != null) + { + toolsets.Add(toolset); } } diff --git a/tools/korebuild.schema.json b/tools/korebuild.schema.json index 9147ba6..1ee9ca8 100644 --- a/tools/korebuild.schema.json +++ b/tools/korebuild.schema.json @@ -8,7 +8,7 @@ { "properties": { "required": { - "description": "Visual Studio is required to build this repo. Defaults to true if not specified.", + "description": "This toolset is required to build this repo. Defaults to true if not specified.", "type": "boolean", "default": true } @@ -17,7 +17,7 @@ { "properties": { "required": { - "description": "Visual Studio is required to build this repo. Defaults to true if not specified.", + "description": "This toolset is required to build this repo. Defaults to true if not specified.", "type": "array", "default": [ "windows", @@ -37,6 +37,23 @@ } ] }, + "nodejs": { + "type": "object", + "description": "Defines the requirements for a NodeJS installation.", + "allOf": [ + { + "$ref": "#/definitions/platforms" + }, + { + "properties": { + "minVersion": { + "type": "string", + "description": "The minimum version of NodeJS required. Must contain at least ., but can also be .." + } + } + } + ] + }, "visualstudio": { "type": "object", "description": "Defines the requirements for Visual Studio installation.", @@ -89,6 +106,9 @@ "properties": { "visualstudio": { "$ref": "#/definitions/visualstudio" + }, + "nodejs": { + "$ref": "#/definitions/nodejs" } } }