public void ValidateAllowlistCaseSensitivity() { FileArtifact exe = GetOsShell(); PathAtom exeNameUpperCase = PathAtom.Create(Context.StringTable, exe.Path.GetName(Context.PathTable).ToString(Context.StringTable).ToUpper()); var exeLinkUnion = new DiscriminatingUnion <FileArtifact, PathAtom>(exeNameUpperCase); Configuration.CacheableFileAccessAllowlist.Add(new Configuration.Mutable.FileAccessAllowlistEntry() { ToolPath = exeLinkUnion, PathRegex = ".*" }); FileArtifact output = CreateOutputFileArtifact(); CreateAndScheduleOsShellPip(exe, output); if (OperatingSystemHelper.IsPathComparisonCaseSensitive) { // This should fail on an OS that has case sensitive paths because we converted the process name to uppercase RunScheduler().AssertFailure(); AssertLogContains(true, $"W {output.Path.ToString(Context.PathTable)}"); AssertErrorEventLogged(LogEventId.FileMonitoringError, 1); AssertWarningEventLogged(LogEventId.ProcessNotStoredToCacheDueToFileMonitoringViolations, 1); } else { RunScheduler().AssertSuccess(); XAssert.AreEqual("hi", File.ReadAllText(ArtifactToString(output)).Trim().Trim('\'')); } }
private static DiscriminatingUnion <AbsolutePath, IInlineModuleDefinition> RemapModule( DiscriminatingUnion <AbsolutePath, IInlineModuleDefinition> fileOrInlineModule, PathRemapper pathRemapper) { var fileOrInlineModuleValue = fileOrInlineModule?.GetValue(); if (fileOrInlineModuleValue == null) { return(null); } if (fileOrInlineModuleValue is AbsolutePath path) { return(new DiscriminatingUnion <AbsolutePath, IInlineModuleDefinition>(pathRemapper.Remap(path))); } var inlineModuleDefinition = (IInlineModuleDefinition)fileOrInlineModuleValue; var remappedInlineModuleDefinition = new InlineModuleDefinition { ModuleName = inlineModuleDefinition.ModuleName, Projects = inlineModuleDefinition.Projects?.Select(project => pathRemapper.Remap(project)).ToList() }; return(new DiscriminatingUnion <AbsolutePath, IInlineModuleDefinition>(remappedInlineModuleDefinition)); }
/// <summary> /// Returns all projects that the actual selector in the discriminating union specifies /// </summary> public bool TryGetMatches(DiscriminatingUnion <string, IJavaScriptProjectSimpleSelector, IJavaScriptProjectRegexSelector> selector, out IReadOnlyCollection <JavaScriptProject> matches, out string failure) { failure = string.Empty; switch (selector.GetValue()) { case string s: matches = GetMatches(s); return(true); case IJavaScriptProjectSimpleSelector simpleSelector: matches = GetMatches(simpleSelector); return(true); case IJavaScriptProjectRegexSelector regexSelector: try { matches = GetMatches(regexSelector); } catch (ArgumentException e) { matches = CollectionUtilities.EmptyArray <JavaScriptProject>(); failure = e.Message; return(false); } return(true); default: Contract.Assert(false, $"Unexpected type {selector.GetValue().GetType()}"); matches = CollectionUtilities.EmptyArray <JavaScriptProject>(); return(false); } }
/// <summary> /// Construct a new allowlist entry that will match based on tool and path. /// </summary> /// <param name="executable">The exact full path or the executable name to the tool that does the bad access.</param> /// <param name="pathRegex">The ECMAScript regex pattern that will be used as the basis for the match.</param> /// <param name="allowsCaching"> /// Whether this allowlist rule should be interpreted to allow caching of a pip that matches /// it. /// </param> /// <param name="name">Name of the allowlist entry. Defaults to 'Unnamed' if null/empty.</param> public ExecutablePathAllowlistEntry(DiscriminatingUnion <AbsolutePath, PathAtom> executable, SerializableRegex pathRegex, bool allowsCaching, string name) : base(pathRegex, allowsCaching, name) { Contract.RequiresNotNull(executable); Contract.Requires(pathRegex != null); Executable = executable; }
public void AllowlistOnSpawnProcess(bool includeExecutableLink) { FileArtifact exeLink = CreateOsShellExecutableSymbolicLink(); var exeLinkUnion = new DiscriminatingUnion <FileArtifact, PathAtom>(exeLink); Configuration.CacheableFileAccessAllowlist.Add(new Configuration.Mutable.FileAccessAllowlistEntry() { ToolPath = exeLinkUnion, PathRegex = ".*" }); FileArtifact output = CreateOutputFileArtifact(); var builder = CreatePipBuilder(new[] { Operation.SpawnExe( Context.PathTable, exeLink, string.Format(OperatingSystemHelper.IsUnixOS ? "-c \"echo 'hi' > {0}\"" : "/d /c echo 'hi' > {0}", output.Path.ToString(Context.PathTable))), }); builder.AddOutputFile(output.Path); if (includeExecutableLink) { builder.AddInputFile(exeLink); } foreach (var dep in CmdHelper.GetCmdDependencies(Context.PathTable)) { builder.AddUntrackedFile(dep); } foreach (var dep in CmdHelper.GetCmdDependencyScopes(Context.PathTable)) { builder.AddUntrackedDirectoryScope(dep); } SchedulePipBuilder(builder); if (includeExecutableLink) { RunScheduler().AssertSuccess(); XAssert.AreEqual("hi", File.ReadAllText(ArtifactToString(output)).Trim().Trim('\'')); } else { RunScheduler().AssertFailure(); // DFA on exeLink because it is not specified as input. // Although there's a cacheable allowlist entry for exeLink, that entry only holds for file accessed by exeLink. // In this case, exeLink is accessed by the test process, so there's a read operation by the test process on exeLink, hence DFA. AssertErrorEventLogged(LogEventId.FileMonitoringError, 1); AssertLogContains(false, $"R {exeLink.Path.ToString(Context.PathTable)}"); AssertWarningEventLogged(LogEventId.ProcessNotStoredToCacheDueToFileMonitoringViolations, 1); } }
private bool ValidateProjectSelector(DiscriminatingUnion <string, IJavaScriptProjectSimpleSelector, IJavaScriptProjectRegexSelector> selector, string pathToFile, string provenance) { object projectValue = selector.GetValue(); switch (projectValue) { case string packageName: { if (string.IsNullOrEmpty(packageName)) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Package name for {provenance} must be defined."); return(false); } break; } case IJavaScriptProjectSimpleSelector simpleSelector: { if (string.IsNullOrEmpty(simpleSelector.PackageName)) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Package name for {provenance} must be defined."); return(false); } if (simpleSelector.Commands == null) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Commands for {provenance} must be defined."); return(false); } foreach (var command in simpleSelector.Commands) { if (string.IsNullOrEmpty(command)) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Command name for {provenance} must be defined."); return(false); } } break; } case IJavaScriptProjectRegexSelector regexSelector: { if (string.IsNullOrEmpty(regexSelector.PackageNameRegex)) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Package name regular expression for {provenance} must be defined."); return(false); } break; } } return(true); }
/// <nodoc/> public CustomJavaScriptResolverSettings(ICustomJavaScriptResolverSettings resolverSettings, PathRemapper pathRemapper) : base(resolverSettings, pathRemapper) { if (resolverSettings.CustomProjectGraph?.GetValue() is AbsolutePath absolutePath) { CustomProjectGraph = new DiscriminatingUnion <AbsolutePath, IReadOnlyDictionary <string, IJavaScriptCustomProjectGraphNode> >(pathRemapper.Remap(absolutePath)); } else { CustomProjectGraph = resolverSettings.CustomProjectGraph; } }
public void ValidateAllowlistWithFullPathOnSpawnedProcess() { FileArtifact exe = GetOsShell(); var exeLinkUnion = new DiscriminatingUnion <FileArtifact, PathAtom>(exe); Configuration.CacheableFileAccessAllowlist.Add(new Configuration.Mutable.FileAccessAllowlistEntry() { ToolPath = exeLinkUnion, PathRegex = ".*" }); FileArtifact output = CreateOutputFileArtifact(); CreateAndScheduleOsShellPip(exe, output); RunScheduler().AssertSuccess(); XAssert.AreEqual("hi", File.ReadAllText(ArtifactToString(output)).Trim().Trim('\'')); }
public void EnvironmentIsHonored() { var one = new DiscriminatingUnion <string, UnitValue>(); one.SetValue("1"); var project = CreateProjectWithPredictions("A.proj"); var testProj = Start(new MsBuildResolverSettings { Environment = new Dictionary <string, DiscriminatingUnion <string, UnitValue> > { ["Test"] = one } }) .Add(project) .ScheduleAll() .AssertSuccess() .RetrieveSuccessfulProcess(project); var testEnvironmentVariable = testProj.EnvironmentVariables.First(e => e.Name.ToString(PathTable.StringTable).Equals("Test")); Assert.Equal("1", testEnvironmentVariable.Value.ToString(PathTable)); }
/// <nodoc /> public DiscriminatingUnion <FileArtifact, PathAtom> Remap(DiscriminatingUnion <FileArtifact, PathAtom> fileUnion) { DiscriminatingUnion <FileArtifact, PathAtom> remappedPath = null; if (fileUnion != null) { var fileValue = fileUnion.GetValue(); remappedPath = new DiscriminatingUnion <FileArtifact, PathAtom>(); if (fileValue is FileArtifact file) { remappedPath.SetValue(Remap(file)); } else if (fileValue is PathAtom pathAtom) { remappedPath.SetValue(Remap(pathAtom)); } } return(remappedPath); }
/// <nodoc /> public DiscriminatingUnion <FileArtifact, IReadOnlyList <DirectoryArtifact> > Remap(DiscriminatingUnion <FileArtifact, IReadOnlyList <DirectoryArtifact> > fileUnion) { DiscriminatingUnion <FileArtifact, IReadOnlyList <DirectoryArtifact> > remappedPath = null; if (fileUnion != null) { var fileValue = fileUnion.GetValue(); remappedPath = new DiscriminatingUnion <FileArtifact, IReadOnlyList <DirectoryArtifact> >(); if (fileValue is FileArtifact file) { remappedPath.SetValue(Remap(file)); } else if (fileValue is IReadOnlyList <DirectoryArtifact> searchDirectories) { remappedPath.SetValue(Remap(searchDirectories)); } } return(remappedPath); }
public void ValidateOverlappingAllowlistToolPaths() { FileArtifact exe = GetOsShell(); var exeLinkFullPath = new DiscriminatingUnion <FileArtifact, PathAtom>(exe); var exeLinkNameOnly = new DiscriminatingUnion <FileArtifact, PathAtom>(exe.Path.GetName(Context.PathTable)); Configuration.CacheableFileAccessAllowlist.Add(new Configuration.Mutable.FileAccessAllowlistEntry() { ToolPath = exeLinkFullPath, PathRegex = ".*" }); Configuration.CacheableFileAccessAllowlist.Add(new Configuration.Mutable.FileAccessAllowlistEntry() { ToolPath = exeLinkNameOnly, PathRegex = ".*" }); FileArtifact output = CreateOutputFileArtifact(); CreateAndScheduleOsShellPip(exe, output); RunScheduler().AssertSuccess(); XAssert.AreEqual("hi", File.ReadAllText(ArtifactToString(output)).Trim().Trim('\'')); }
private static DirectoryArtifact ResolveAbsoluteOrRelativeDirectory(PathTable pathTable, DiscriminatingUnion <DirectoryArtifact, RelativePath> absoluteOrRelativeUnion, AbsolutePath root) { var absoluteOrRelative = absoluteOrRelativeUnion.GetValue(); if (absoluteOrRelative is DirectoryArtifact directory) { return(directory); } var relative = (RelativePath)absoluteOrRelative; if (!relative.IsValid) { return(DirectoryArtifact.Invalid); } return(DirectoryArtifact.CreateWithZeroPartialSealId(root.Combine(pathTable, relative))); }
public async Task TestSerialization() { var context = BuildXLContext.CreateInstanceForTesting(); var pathTable = context.PathTable; var stringTable = context.StringTable; var symbolTable = new SymbolTable(pathTable.StringTable); var allowlist = new FileAccessAllowlist(context); //Allowlist with full paths var path1 = new DiscriminatingUnion <AbsolutePath, PathAtom>(AbsolutePath.Create(pathTable, @"\\fakePath\foo.txt")); var path2 = new DiscriminatingUnion <AbsolutePath, PathAtom>(AbsolutePath.Create(pathTable, @"\\fakePath\bar.txt")); var regex1 = new SerializableRegex(@"dir\foo.txt"); var executableEntry1 = new ExecutablePathAllowlistEntry( path1, regex1, true, "entry1"); var executableEntry2 = new ExecutablePathAllowlistEntry( path2, new SerializableRegex("bar"), false, "entry2"); allowlist.Add(executableEntry1); allowlist.Add(executableEntry2); // Allowlist with executable names only var path3 = new DiscriminatingUnion <AbsolutePath, PathAtom>(PathAtom.Create(stringTable, "alice.txt")); var path4 = new DiscriminatingUnion <AbsolutePath, PathAtom>(PathAtom.Create(stringTable, "bob.txt")); var regex3 = new SerializableRegex(@"dir\alice.txt"); var executableEntry3 = new ExecutablePathAllowlistEntry( path3, regex3, true, "entry5"); var executableEntry4 = new ExecutablePathAllowlistEntry( path4, new SerializableRegex("bob"), false, "entry6"); allowlist.Add(executableEntry3); allowlist.Add(executableEntry4); var symbol1 = FullSymbol.Create(symbolTable, "symbol1"); var valueEntry = new ValuePathFileAccessAllowlistEntry( symbol1, new SerializableRegex("symbol1"), false, null); var symbol2 = FullSymbol.Create(symbolTable, "symbol2"); var valueEntry2 = new ValuePathFileAccessAllowlistEntry( symbol2, new SerializableRegex("symbol2"), false, "entry4"); allowlist.Add(valueEntry); allowlist.Add(valueEntry2); XAssert.AreEqual(4, allowlist.UncacheableEntryCount); XAssert.AreEqual(2, allowlist.CacheableEntryCount); XAssert.AreEqual("Unnamed", valueEntry.Name); using (var ms = new MemoryStream()) { BuildXLWriter writer = new BuildXLWriter(true, ms, true, true); allowlist.Serialize(writer); ms.Position = 0; BuildXLReader reader = new BuildXLReader(true, ms, true); var deserialized = await FileAccessAllowlist.DeserializeAsync(reader, Task.FromResult <PipExecutionContext>(context)); var path1Absolute = (AbsolutePath)path1.GetValue(); var path2Absolute = (AbsolutePath)path2.GetValue(); var path3Atom = ((PathAtom)path3.GetValue()).StringId; var path4Atom = ((PathAtom)path4.GetValue()).StringId; XAssert.AreEqual(2, deserialized.ExecutablePathEntries.Count); XAssert.AreEqual(1, deserialized.ExecutablePathEntries[path1Absolute].Count); XAssert.AreEqual(true, deserialized.ExecutablePathEntries[path1Absolute][0].AllowsCaching); XAssert.AreEqual(regex1.ToString(), deserialized.ExecutablePathEntries[path1Absolute][0].PathRegex.ToString()); XAssert.AreEqual(executableEntry1.Name, deserialized.ExecutablePathEntries[path1Absolute][0].Name); XAssert.AreEqual(executableEntry2.Name, deserialized.ExecutablePathEntries[path2Absolute][0].Name); XAssert.AreEqual(2, deserialized.ToolExecutableNameEntries.Count); XAssert.AreEqual(1, deserialized.ToolExecutableNameEntries[path3Atom].Count); XAssert.AreEqual(true, deserialized.ToolExecutableNameEntries[path3Atom][0].AllowsCaching); XAssert.AreEqual(regex3.ToString(), deserialized.ToolExecutableNameEntries[path3Atom][0].PathRegex.ToString()); XAssert.AreEqual(executableEntry3.Name, deserialized.ToolExecutableNameEntries[path3Atom][0].Name); XAssert.AreEqual(executableEntry4.Name, deserialized.ToolExecutableNameEntries[path4Atom][0].Name); XAssert.AreEqual(2, deserialized.ValuePathEntries.Count); XAssert.AreEqual(1, deserialized.ValuePathEntries[symbol1].Count); XAssert.AreEqual(false, deserialized.ValuePathEntries[symbol1][0].AllowsCaching); XAssert.AreEqual(valueEntry.Name, deserialized.ValuePathEntries[symbol1][0].Name); XAssert.AreEqual(valueEntry2.Name, deserialized.ValuePathEntries[symbol2][0].Name); XAssert.AreEqual(4, deserialized.UncacheableEntryCount); XAssert.AreEqual(2, deserialized.CacheableEntryCount); } }
/// <nodoc/> protected virtual bool ValidateResolverSettings(TResolverSettings resolverSettings) { var pathToFile = resolverSettings.File.ToString(Context.PathTable); if (!resolverSettings.Root.IsValid) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), "The root must be specified."); return(false); } if (string.IsNullOrEmpty(resolverSettings.ModuleName)) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), "The module name must not be empty."); return(false); } if (resolverSettings.CustomCommands != null) { var commandNames = new HashSet <string>(); foreach (var customCommand in resolverSettings.CustomCommands) { if (string.IsNullOrEmpty(customCommand.Command)) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), "A non-empty custom command name must be defined."); return(false); } if (!commandNames.Add(customCommand.Command)) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Duplicated custom command name '{customCommand.Command}'."); return(false); } if (customCommand.ExtraArguments == null) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Extra arguments for custom command '{customCommand.Command}' must be defined."); return(false); } } } if (resolverSettings.Exports != null) { var symbolNames = new HashSet <FullSymbol>(); foreach (var javaScriptExport in resolverSettings.Exports) { if (!javaScriptExport.SymbolName.IsValid) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Symbol name is undefined."); return(false); } if (!symbolNames.Add(javaScriptExport.SymbolName)) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Duplicate symbol name '{javaScriptExport.SymbolName.ToString(Context.SymbolTable)}'."); return(false); } // Each specified project must be non-empty foreach (var selector in javaScriptExport.Content) { if (!ValidateProjectSelector(selector, pathToFile, $"JavaScript export '{javaScriptExport.SymbolName.ToString(Context.SymbolTable)}'")) { return(false); } } } } if (resolverSettings.CustomScheduling != null) { if (string.IsNullOrEmpty(resolverSettings.CustomScheduling.Module)) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Module name for the custom scheduling entry must be defined."); return(false); } if (string.IsNullOrEmpty(resolverSettings.CustomScheduling.SchedulingFunction)) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Custom scheduling function name must be defined."); return(false); } if (FullSymbol.TryCreate(Context.SymbolTable, resolverSettings.CustomScheduling.SchedulingFunction, out _, out _) != FullSymbol.ParseResult.Success) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Custom scheduling function name is not a valid dotted identifier."); return(false); } } if (resolverSettings.AdditionalDependencies != null) { foreach (var additionalDependency in resolverSettings.AdditionalDependencies) { if (additionalDependency.Dependents == null) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Dependents is undefined."); return(false); } if (additionalDependency.Dependencies == null) { Tracing.Logger.Log.InvalidResolverSettings(Context.LoggingContext, Location.FromFile(pathToFile), $"Dependencies is undefined."); return(false); } foreach (var dependent in additionalDependency.Dependents) { if (!ValidateProjectSelector(dependent, pathToFile, "JavaScript dependent")) { return(false); } } foreach (var dependency in additionalDependency.Dependencies.Where(dependency => dependency.GetValue() is not ILazyEval)) { var javaScriptProjectSelector = new DiscriminatingUnion <string, IJavaScriptProjectSimpleSelector, IJavaScriptProjectRegexSelector>(); javaScriptProjectSelector.TrySetValue(dependency.GetValue()); if (!ValidateProjectSelector(javaScriptProjectSelector, pathToFile, "JavaScript dependency")) { return(false); } } } } return(true); }
private static bool ComputeCommands( LoggingContext context, Location location, IReadOnlyList<DiscriminatingUnion<string, IJavaScriptCommand>> commands, out IReadOnlyDictionary<string, IReadOnlyList<IJavaScriptCommandDependency>> resultingCommands) { if (commands == null) { // If not defined, the default is ["build"] commands = new[] { new DiscriminatingUnion<string, IJavaScriptCommand>("build") }; } var computedCommands = new Dictionary<string, IReadOnlyList<IJavaScriptCommandDependency>>(commands.Count); resultingCommands = computedCommands; for (int i = 0; i < commands.Count; i++) { DiscriminatingUnion<string, IJavaScriptCommand> command = commands[i]; string commandName = command.GetCommandName(); if (string.IsNullOrEmpty(commandName)) { Tracing.Logger.Log.JavaScriptCommandIsEmpty(context, location); return false; } if (computedCommands.ContainsKey(commandName)) { Tracing.Logger.Log.JavaScriptCommandIsDuplicated(context, location, commandName); return false; } if (command.GetValue() is string simpleCommand) { // A simple string command first on the list means depending on the same command of all its dependencies. // Canonical example: 'build' if (i == 0) { computedCommands.Add( simpleCommand, new[] { new JavaScriptCommandDependency { Command = simpleCommand, Kind = JavaScriptCommandDependency.Package } }); } else { // A simple string command that is not first in the list means depending on the immediate predecesor in the list // Canonical example: 'build', 'test' computedCommands.Add( simpleCommand, new[] { new JavaScriptCommandDependency { Command = commands[i - 1].GetCommandName(), Kind = JavaScriptCommandDependency.Local } }); } } else { // Otherwise if a full fledge command is specified, then we honor it as is var javaScriptCommand = (IJavaScriptCommand)command.GetValue(); computedCommands.Add(commandName, javaScriptCommand.DependsOn); } } return true; }
private static bool ComputeCommands( LoggingContext context, Location location, IReadOnlyList <DiscriminatingUnion <string, IJavaScriptCommand, IJavaScriptCommandGroupWithDependencies, IJavaScriptCommandGroup> > commands, out IReadOnlyDictionary <string, IReadOnlyList <IJavaScriptCommandDependency> > resultingCommands, out IReadOnlyDictionary <string, IReadOnlyList <string> > resultingCommandGroups) { if (commands == null) { // If not defined, the default is ["build"] commands = new[] { new DiscriminatingUnion <string, IJavaScriptCommand, IJavaScriptCommandGroupWithDependencies, IJavaScriptCommandGroup>("build") }; } var computedCommands = new Dictionary <string, IReadOnlyList <IJavaScriptCommandDependency> >(commands.Count); resultingCommands = computedCommands; var commandGroups = new Dictionary <string, IReadOnlyList <string> >(); resultingCommandGroups = commandGroups; for (int i = 0; i < commands.Count; i++) { DiscriminatingUnion <string, IJavaScriptCommand, IJavaScriptCommandGroupWithDependencies, IJavaScriptCommandGroup> command = commands[i]; string commandName = command.GetCommandName(); if (string.IsNullOrEmpty(commandName)) { Tracing.Logger.Log.JavaScriptCommandIsEmpty(context, location); return(false); } if (computedCommands.ContainsKey(commandName)) { Tracing.Logger.Log.JavaScriptCommandIsDuplicated(context, location, commandName); return(false); } object commandValue = command.GetValue(); if (commandValue is string simpleCommand) { // A simple string command first on the list means depending on the same command of all its dependencies. // Canonical example: 'build' if (i == 0) { computedCommands.Add( simpleCommand, new[] { new JavaScriptCommandDependency { Command = simpleCommand, Kind = JavaScriptCommandDependency.Package } }); } else { // A simple string command that is not first in the list means depending on the immediate predecesor in the list // Canonical example: 'build', 'test' computedCommands.Add( simpleCommand, new[] { new JavaScriptCommandDependency { Command = commands[i - 1].GetCommandName(), Kind = JavaScriptCommandDependency.Local } }); } } else { // Otherwise if a full fledge command is specified, we honor it as is // The command may specify dependencies, in which case we add it to the map // of computed commands. Cases like the Lage resolver explicitly don't expose // commands with dependencies since it is Lage the one that defines them if (commandValue is IJavaScriptCommandWithDependencies commandWithDependencies) { computedCommands.Add(commandName, commandWithDependencies.DependsOn); } // Deal with the case of group commands if (commandValue is IJavaScriptCommandGroup commandGroup) { var emptyCommands = commandGroup.Commands.Where(command => string.IsNullOrEmpty(command)); if (emptyCommands.Any()) { Tracing.Logger.Log.JavaScriptCommandIsEmpty(context, location); return(false); } // Check that command members cannot be groups commands as well var dup = commandGroup.Commands.FirstOrDefault(command => computedCommands.ContainsKey(command)); if (dup != default) { Tracing.Logger.Log.JavaScriptCommandGroupCanOnlyContainRegularCommands(context, location, commandGroup.CommandName, dup); return(false); } commandGroups.Add(commandGroup.CommandName, commandGroup.Commands); } } } return(true); }