-
Notifications
You must be signed in to change notification settings - Fork 74
Create initial prototype of dotnet-user-secrets with MSBuild support #178
Conversation
var sdk = sdkResolver.ResolveProjectSdk(projectDir); | ||
|
||
// workaround limitations in using MSBuild API | ||
Environment.SetEnvironmentVariable("MSBUILD_EXE_PATH", Path.Combine(sdk.BasePath, "MSBuild.exe")); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.... :-/
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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")); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason you're not using GetPropertyValue
?
https://msdn.microsoft.com/en-us/library/microsoft.build.evaluation.project.getpropertyvalue.aspx
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
- public types in internal namespace;
- a few more empty lines to improve readability.
|
||
namespace Microsoft.Extensions.ProjectModel.Internal | ||
{ | ||
internal class DotNetCoreSdk |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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")); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this 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.", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?"
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")); |
There was a problem hiding this comment.
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 = "" } }; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 😄
There was a problem hiding this comment.
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.
🆙 📅 responded to much of the feedback above. Refactored MSBuild evaluation into reusable components. |
There was a problem hiding this 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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
17fe71a
to
7179e6b
Compare
🆙 📅 rebased on top of project model abstractions. |
|
||
namespace Microsoft.Extensions.SecretManager.Tools | ||
{ | ||
internal class MsBuildProjectFinder |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NTaylorMullen added to #181
7179e6b
to
5e1fce4
Compare
Any reason to not support both project.json and msbuild? |
20a16a4
to
dde0229
Compare
5e1fce4
to
f067ae0
Compare
@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. |
f067ae0
to
688a0ab
Compare
Ping @NTaylorMullen any other changes needed before I merge to feature/msbuild ? |
There was a problem hiding this 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
@@ -134,59 +136,37 @@ internal int RunInternal(params string[] args) | |||
CommandOutputProvider.LogLevel = LogLevel.Debug; | |||
} | |||
|
|||
var userSecretsId = ResolveUserSecretsId(options); | |||
var userSecretsId = !string.IsNullOrEmpty(options.Id) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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;
There was a problem hiding this comment.
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-*", |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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