Skip to content

Commit

Permalink
Use out-of-proc MSBuild evaluation (dotnet#674)
Browse files Browse the repository at this point in the history
* Use out-of-proc MSBuild evaluation

This allows for support for new SDKs/TFMs without needing tye to target those TFMs

* Combine restore and metadata evaluation

* Batch process projects
  • Loading branch information
John Luo authored Sep 28, 2020
1 parent f0c4f2f commit bbef222
Show file tree
Hide file tree
Showing 18 changed files with 267 additions and 164 deletions.
3 changes: 3 additions & 0 deletions clean.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@ECHO OFF
SETLOCAL
PowerShell -NoProfile -NoLogo -ExecutionPolicy ByPass -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = ''; try { & '%~dp0clean.ps1' %*; exit $LASTEXITCODE } catch { write-host $_; exit 1 }"
42 changes: 42 additions & 0 deletions clean.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#requires -version 5

<#
.SYNOPSIS
Clean this repository.
.DESCRIPTION
This script cleans this repository interactively, leaving downloaded infrastructure untouched.
Clean operation is interactive to avoid losing new but unstaged files. Press 'c' then [Enter]
to perform the proposed deletions.
.EXAMPLE
Perform default clean operation.
clean.ps1
.EXAMPLE
Clean everything but downloaded infrastructure and VS / VS Code folders.
clean.ps1 -e .vs/ -e .vscode/
#>

[CmdletBinding(PositionalBinding = $false)]
param(
# Other lifecycle targets
[switch]$Help, # Show help

# Capture the rest
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$GitArguments
)

Set-StrictMode -Version 2
$ErrorActionPreference = 'Stop'

if ($Help) {
Get-Help $PSCommandPath
exit 0
}

git clean -dix -e .dotnet/ -e .tools/ @GitArguments
git checkout -- $(git ls-files -d)
38 changes: 38 additions & 0 deletions clean.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash

set -euo pipefail

#
# Functions
#
__usage() {
echo "Usage: $(basename "${BASH_SOURCE[0]}") <Arguments>
Arguments:
<Arguments>... Arguments passed to the 'git' command. Any number of arguments allowed.
Description:
This script cleans the repository interactively, leaving downloaded infrastructure untouched.
Clean operation is interactive to avoid losing new but unstaged files. Press 'c' then [Enter]
to perform the proposed deletions.
"
}

git_args=()

while [[ $# -gt 0 ]]; do
case $1 in
-\?|-h|--help)
__usage
exit 0
;;
*)
git_args[${#git_args[*]}]="$1"
;;
esac
shift
done

# This incantation avoids unbound variable issues if git_args is empty
# https://stackoverflow.com/questions/7577052/bash-empty-array-expansion-with-set-u
git clean -dix -e .dotnet/ -e .tools/ ${git_args[@]+"${git_args[@]}"}
97 changes: 93 additions & 4 deletions src/Microsoft.Tye.Core/ApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Tye.ConfigModel;

Expand Down Expand Up @@ -74,6 +77,88 @@ public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, F
config.Services.Where(filter.ServicesFilter).ToList() :
config.Services;

var sw = Stopwatch.StartNew();
// Project services will be restored and evaluated before resolving all other services.
// This batching will mitigate the performance cost of running MSBuild out of process.
var projectServices = services.Where(s => !string.IsNullOrEmpty(s.Project));
var projectMetadata = new Dictionary<string, string>();

using (var directory = TempDirectory.Create())
{
var projectPath = Path.Combine(directory.DirectoryPath, Path.GetRandomFileName() + ".proj");

var sb = new StringBuilder();
sb.AppendLine("<Project>");
sb.AppendLine(" <ItemGroup>");

foreach (var project in projectServices)
{
var expandedProject = Environment.ExpandEnvironmentVariables(project.Project!);
project.ProjectFullPath = Path.Combine(config.Source.DirectoryName!, expandedProject);

if (!File.Exists(project.ProjectFullPath))
{
throw new CommandException($"Failed to locate project: '{project.ProjectFullPath}'.");
}

sb.AppendLine($" <MicrosoftTye_ProjectServices " +
$"Include=\"{project.ProjectFullPath}\" " +
$"Name=\"{project.Name}\" " +
$"BuildProperties=\"{(project.BuildProperties.Any() ? project.BuildProperties.Select(kvp => $"{kvp.Name}={kvp.Value}").Aggregate((a, b) => a + ";" + b) : string.Empty)}\" />");
}
sb.AppendLine(@" </ItemGroup>");

sb.AppendLine($@" <Target Name=""MicrosoftTye_EvaluateProjects"">");
sb.AppendLine($@" <MsBuild Projects=""@(MicrosoftTye_ProjectServices)"" "
+ $@"Properties=""%(BuildProperties);"
+ $@"MicrosoftTye_ProjectName=%(Name)"" "
+ $@"Targets=""MicrosoftTye_GetProjectMetadata"" BuildInParallel=""true"" />");

sb.AppendLine(" </Target>");
sb.AppendLine("</Project>");
File.WriteAllText(projectPath, sb.ToString());

output.WriteDebugLine("Restoring and evaluating projects");

var msbuildEvaluationResult = await ProcessUtil.RunAsync(
"dotnet",
$"build " +
$"\"{projectPath}\" " +
$"/p:CustomAfterMicrosoftCommonTargets={Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ProjectEvaluation.targets")} " +
$"/nologo",
throwOnError: false,
workingDirectory: directory.DirectoryPath);

// If the build fails, we're not really blocked from doing our work.
// For now we just log the output to debug. There are errors that occur during
// running these targets we don't really care as long as we get the data.
if (msbuildEvaluationResult.ExitCode != 0)
{
output.WriteDebugLine($"Evaluating project failed with exit code {msbuildEvaluationResult.ExitCode}:" +
$"{Environment.NewLine}Ouptut: {msbuildEvaluationResult.StandardOutput}" +
$"{Environment.NewLine}Error: {msbuildEvaluationResult.StandardError}");
}

var msbuildEvaluationOutput = msbuildEvaluationResult
.StandardOutput
.Split(Environment.NewLine);

foreach (var line in msbuildEvaluationOutput)
{
if (line.Trim().StartsWith("Microsoft.Tye metadata: "))
{
var values = line.Split(':', 3);
var projectName = values[1].Trim();
var metadataPath = values[2].Trim();
projectMetadata.Add(projectName, metadataPath);

output.WriteDebugLine($"Resolved metadata for service {projectName} at {metadataPath}");
}
}

output.WriteDebugLine($"Restore and project evaluation took: {sw.Elapsed.TotalMilliseconds}ms");
}

foreach (var configService in services)
{
ServiceBuilder service;
Expand All @@ -87,9 +172,7 @@ public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, F

if (!string.IsNullOrEmpty(configService.Project))
{
var expandedProject = Environment.ExpandEnvironmentVariables(configService.Project);
var projectFile = new FileInfo(Path.Combine(config.Source.DirectoryName!, expandedProject));
var project = new DotnetProjectServiceBuilder(configService.Name!, projectFile);
var project = new DotnetProjectServiceBuilder(configService.Name!, new FileInfo(configService.ProjectFullPath));
service = project;

project.Build = configService.Build ?? true;
Expand All @@ -107,7 +190,13 @@ public static async Task<ApplicationBuilder> CreateAsync(OutputContext output, F
// to prompt for the registry name.
project.ContainerInfo = new ContainerInfo() { UseMultiphaseDockerfile = false, };

await ProjectReader.ReadProjectDetailsAsync(output, project);
// If project evaluation is successful this should not happen, therefore an exception will be thrown.
if (!projectMetadata.ContainsKey(configService.Name))
{
throw new CommandException($"Evaluated project metadata file could not be found for service {configService.Name}");
}

ProjectReader.ReadProjectDetails(output, project, projectMetadata[configService.Name]);

// Do k8s by default.
project.ManifestInfo = new KubernetesManifestInfo();
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class ConfigService
public Dictionary<string, string> DockerFileArgs { get; set; } = new Dictionary<string, string>();
public string? DockerFileContext { get; set; }
public string? Project { get; set; }
public string? ProjectFullPath { get; set; }
public string? Include { get; set; }
public string? Repository { get; set; }
public bool? Build { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.Tye.Core/Microsoft.Tye.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>Tye</RootNamespace>
<AssemblyName>Microsoft.Tye.Core</AssemblyName>
<PackageId>Microsoft.Tye.Core</PackageId>
Expand Down
Loading

0 comments on commit bbef222

Please sign in to comment.