Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Create initial prototype of dotnet-user-secrets with MSBuild support #178

Merged
merged 3 commits into from
Oct 7, 2016

Conversation

natemcmaster
Copy link
Contributor

@natemcmaster natemcmaster commented Sep 28, 2016

A prototype port of "dotnet-user-secrets" from project.json to MSBuild support.

This CLI project tool requires the ability to evaluate the project file to find the 'UserSecretsId' property. At the moment, this change will introduce a dependency on the existence and location of the .NET Core SDK. Without this, we cannot properly set the "MSBuildExtensionsPath" property which leads to project evaluation failing on initialization.

NB: merging to feature/msbuild, not dev. More refinement needed before this is ready for a nightly version.

TODO:

cc @NTaylorMullen @bricelam @prafullbhosale @davidfowl @jeffkl @eerhardt

var sdk = sdkResolver.ResolveProjectSdk(projectDir);

// workaround limitations in using MSBuild API
Environment.SetEnvironmentVariable("MSBUILD_EXE_PATH", Path.Combine(sdk.BasePath, "MSBuild.exe"));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting it looks like you're reusing the MSBuild that ships with the dotnet CLI is that correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's the only way I've been able to get this working.... :-/

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this running as one of these "project tools" where it's not deployed via dotnet publish then?

Or do you get errors about missing .props or .targets files?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this one of those "project tools". It's deployed into a user project by dotnet-cli with that funky ".nuget/packages/.tools/" folder magic.

The error is dotnet/msbuild#1097. I've also attempted to use a version of MSBuild that is shipped with my tool, but have found issues getting all of MSBuild dependencies into one place (https://github.com/Microsoft/msbuild/issues/1096)...which is why for now this prototype finds the MSBuild inside the .NET Core SDK.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay yeah we don't have a good story yet for these project tools. I guess this is valid but hopefully this isn't abused.

I'm also working on a change to build MSBuild.dll instead of MSBuild.exe like we should be. This would break this tool if you try and take a newer package and I'm still investigating who else will be broken.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is valid but hopefully this isn't abused.

IIUC this is exists primarily as a test hook. I'm kind of hoping dotnet/msbuild#1097 results in some API that we can use to tell MSBuild "here's the runtime you should use".

var sdk = sdkResolver.ResolveProjectSdk(projectDir);

// workaround limitations in using MSBuild API
Environment.SetEnvironmentVariable("MSBUILD_EXE_PATH", Path.Combine(sdk.BasePath, "MSBuild.exe"));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay yeah we don't have a good story yet for these project tools. I guess this is valid but hopefully this isn't abused.

I'm also working on a change to build MSBuild.dll instead of MSBuild.exe like we should be. This would break this tool if you try and take a newer package and I'm still investigating who else will be broken.

project = new Project(xml, globalProperties, /* toolsVersion: */ null, projectCollection);
}

var userSecretsId = project.Properties.FirstOrDefault(p => p.Name == "UserSecretsId")?.EvaluatedValue;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed it would throw if the property didn't exist, but the API doc says otherwise, so I'll update to use that API.

{
internal class MsBuildProjectReader
{
public string ReadUserSecretId(IFileInfo fileInfo)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I wrote this class, I would have it's constructor take IFileInfo, add a private method like GetPropertyValue(string name), and change ReadUserSecretId() to call it passing "UserSecretsId". This way you could add new methods to read new properties. But I have no idea the future of this tool so do what you want.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah...i'm kind of intentionally trying to make this class useless to others at the moment. I don't want others to copy-paste without understanding its implementation....plus I'm not sure the implementation is good...still learning which MSBuild APIs are appropriate to use.

But once I'm more confident in my MSBuild-fu, I'll make changes as you suggested and will likely introduce a implementation that can be shared with all aspnet tools that need to evaluate projects.

Copy link

@troydai troydai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things to address.

  1. public types in internal namespace;
  2. a few more empty lines to improve readability.


namespace Microsoft.Extensions.ProjectModel.Internal
{
internal class DotNetCoreSdk
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as far as i remember, we keep the types in *.Internal public. it is to avoid over-internalized types but explicitly states that types in *.Internal namespace are subject to change.


namespace Microsoft.Extensions.ProjectModel.Internal
{
internal class DotNetCoreSdkResolver
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above.


public DotNetCoreSdkResolver(string installationDir)
{
_installationPath = installationDir;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not keep the names consistent?


public DotNetCoreSdk ResolveProjectSdk(string projectDir)
{
var global = ResolveGlobalJsonSdkVersion(projectDir);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a better variable name: sdkVersion

private IEnumerable<string> Installed
=> Directory.EnumerateDirectories(Path.Combine(_installationPath, "sdk"));

private string ResolveGlobalJsonSdkVersion(string start)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

position private method behind public methods.

}
}

public DotNetCoreSdk ResolveLatest()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this method be private?

Logger.LogDebug(Resources.Message_Project_File_Path, fileInfo.PhysicalPath);
return ReadUserSecretsId(fileInfo);
}
var physicalFile = new PhysicalFileInfo(fileInfo);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create this instance after test if fileInfo exists.

{
internal class MsBuildProjectReader
{
public string ReadUserSecretId(IFileInfo fileInfo)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need an IFileInfo or could you just use Stream instead (and maybe keep this overload too)? It makes testing easier

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IFileInfo helps also capture info about the directory which we need to ensure we get the right SDK version.

var sdk = sdkResolver.ResolveProjectSdk(projectDir);

// workaround limitations in using MSBuild API
Environment.SetEnvironmentVariable("MSBUILD_EXE_PATH", Path.Combine(sdk.BasePath, "MSBuild.exe"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note here in case you plan to use this code in dotnet watch. When you spin a new child process, it inherits the environment variables. Not sure if this will impact the watched app but it might have some interesting side effects

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it will be too big an issues as dotnet-watch will shell out to the SDK which takes care of ensuring environment variables are set correctly before MSBuild.exe is invoked.

Copy link
Contributor

@NTaylorMullen NTaylorMullen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -33,6 +34,11 @@ public static CommandLineOptions Parse(string[] args, TextWriter output)
var optionProject = app.Option("-p|--project <PROJECT>", "Path to project, default is current directory",
CommandOptionType.SingleValue, inherited: true);

// the escape hatch if project evaluation fails, or if users want to alter a secret store other than the one
// in the current project
var optionId = app.Option("--id", "The user secret id to use.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would project evaluation fail? Trying to determine if we really need this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would project evaluation fail?

At the moment, the question is "When does project evaluation not fail?" :trollface:

Bad XML, missing imports, invalid syntax, etc... there are lots of reasons why it's good to have a an escape hatch. Plus, some may prefer to just us --id and avoid the MSBuild complexity.

}

private IEnumerable<string> Installed
=> Directory.EnumerateDirectories(Path.Combine(_installationPath, "sdk"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the fact that we're already tying this expectation to an SDK folder there's no value in taking in the installationDir. Lets group all of our questionable "where is msbuild.exe" assumptions in this one class.

{
return null;
}
var t = new { sdk = new { version = "" } };
Copy link
Contributor

@NTaylorMullen NTaylorMullen Sep 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make a private class, this is super hacky. That will enable you to not declare this here. See next coment

var t = new { sdk = new { version = "" } };
try
{
var obj = JsonConvert.DeserializeAnonymousType(File.ReadAllText(globalJson.FullName), t);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Break this statement apart. Aka:

var globalJsonContents = File.ReadAlText(globalJson.FullName);
var globalJson =JsonConvert.Deserialize<GlobalJson>(globalJsonContents);

{
var first = Installed.Select(d => new { path = d, version = SemanticVersion.Parse(Path.GetFileName(d)) })
.OrderByDescending(sdk => sdk.version)
.First();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to handle cases when SDK's haven't been installed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory, it's not possible for "dotnet user-secrets" to ever be invoked without some SDK installed...but at the very least we need a better error message for that.

};
}

return ResolveLatest();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Odd that above you're newing up a DotNetCoreSDK in the case that you can resolve a globaljson sdk but not in this case. It's inconsistent.

If you're going to have the ResolveLatest/ResolveGlobalJsonSdkVersion methods separate you should normalize their output.


namespace Microsoft.Extensions.SecretManager.Tools.Internal
{
internal class MsBuildProjectReader
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't look like this class has any state. Any value in it not being static?

{
projectPath = Path.Combine(projectPath, "project.json");
var projects = directoryInfo.EnumerateFileSystemInfos("*.csproj").ToList();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should normalize all this logic across our "complex" tools. Also, your repo where you normalize project.json and MSBuild project reading seem like the correct approach to move forward. Aka, our tools need to continue supporting both project.json and MSBuild for a little while, might as well make the process painless for all 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm all for recreating some kind of "ProjectResolver" that finds either project.json or msbuild files.

@natemcmaster
Copy link
Contributor Author

🆙 📅 responded to much of the feedback above. Refactored MSBuild evaluation into reusable components.

Copy link
Contributor

@NTaylorMullen NTaylorMullen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prototype seems alright, will get cleaned up even more when project model abstractions comes through.


public MsBuildProjectContextFactory()
{
var dotNetHome = Path.GetDirectoryName(new Muxer().MuxerPath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not group this logic in the DotNetCoreSdkResolver?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #180 I have moved this into an abstraction that represents MsBuildContext

@natemcmaster natemcmaster force-pushed the namc/usersecrets-msbuild branch from 17fe71a to 7179e6b Compare October 5, 2016 19:15
@natemcmaster natemcmaster changed the base branch from feature/msbuild to namc/up-msbuild October 5, 2016 19:15
@natemcmaster
Copy link
Contributor Author

🆙 📅 rebased on top of project model abstractions.


namespace Microsoft.Extensions.SecretManager.Tools
{
internal class MsBuildProjectFinder
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be useful in project model abstractions too?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. If you put it into PR i'll review it quickly 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NTaylorMullen added to #181

@natemcmaster natemcmaster force-pushed the namc/usersecrets-msbuild branch from 7179e6b to 5e1fce4 Compare October 5, 2016 20:31
@NTaylorMullen
Copy link
Contributor

Any reason to not support both project.json and msbuild?

@natemcmaster natemcmaster changed the base branch from namc/up-msbuild to feature/msbuild October 5, 2016 22:18
@natemcmaster natemcmaster force-pushed the namc/usersecrets-msbuild branch from 5e1fce4 to f067ae0 Compare October 5, 2016 22:40
@natemcmaster
Copy link
Contributor Author

@NTaylorMullen I would assume we won't want to support both PJ and MSBuild, but I've opened that up for discussion with our PMs.

@natemcmaster natemcmaster force-pushed the namc/usersecrets-msbuild branch from f067ae0 to 688a0ab Compare October 6, 2016 21:23
@natemcmaster
Copy link
Contributor Author

Ping @NTaylorMullen any other changes needed before I merge to feature/msbuild ?

Copy link
Contributor

@NTaylorMullen NTaylorMullen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few unimportant questions but overall :shipit:

@@ -134,59 +136,37 @@ internal int RunInternal(params string[] args)
CommandOutputProvider.LogLevel = LogLevel.Debug;
}

var userSecretsId = ResolveUserSecretsId(options);
var userSecretsId = !string.IsNullOrEmpty(options.Id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: change order for clarity:

var userSecretsId = string.IsNullOrEmpty(options.Id) ? ResolveIdFromProject(options.Project) : options.Id;

.AsDesignTimeBuild()
.WithBuildTargets(Array.Empty<string>())
.WithProjectFile(projectFile)
.WithTargetFramework("") // TFM doesn't matter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to set it then? Seems like this should be a default

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tool doesn't need to worry about TFMs. We just want to read a global property. The alternative would be .BuildAllTargetFrameworks().First()

.WithTargetFramework("") // TFM doesn't matter
.Build();

return project.GetUserSecretsId();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make debugging easier

var id = project.GetUserSecretsId();
return id;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this make debugging easier?

"type": "build",
"version": "1.0.0-*"
},
"Microsoft.Extensions.ProjectModel.Sources": "1.0.0-*",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not build time dependency?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Less typing (shrugs lazily). It's a test project so doesn't matter either way.

@natemcmaster natemcmaster merged commit ef63762 into feature/msbuild Oct 7, 2016
@natemcmaster natemcmaster deleted the namc/usersecrets-msbuild branch October 7, 2016 16:56
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants