2015-12-10 23:00:35 -08:00
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using Microsoft.DotNet.Cli.Utils ;
using Microsoft.DotNet.ProjectModel ;
using Microsoft.DotNet.ProjectModel.Compilation ;
using NuGet.Frameworks ;
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
2016-02-03 10:57:25 -08:00
using Microsoft.DotNet.Cli.Compiler.Common ;
2015-12-18 16:39:43 -08:00
using Microsoft.Extensions.PlatformAbstractions ;
2016-02-02 10:50:59 -08:00
using Microsoft.DotNet.Files ;
2016-01-08 16:20:39 -08:00
using Microsoft.DotNet.Tools.Common ;
2016-01-11 13:40:47 -08:00
using Microsoft.DotNet.ProjectModel.Utilities ;
2016-03-25 15:40:16 -07:00
using Microsoft.DotNet.ProjectModel.Graph ;
using NuGet.Versioning ;
2015-12-10 23:00:35 -08:00
namespace Microsoft.DotNet.Tools.Publish
{
2016-01-30 21:47:50 -08:00
public partial class PublishCommand
2015-12-10 23:00:35 -08:00
{
2016-02-16 15:30:39 -08:00
private const string PublishSubfolderName = "publish" ;
2015-12-10 23:00:35 -08:00
public string ProjectPath { get ; set ; }
public string Configuration { get ; set ; }
2016-02-03 10:57:25 -08:00
public string BuildBasePath { get ; set ; }
2015-12-10 23:00:35 -08:00
public string OutputPath { get ; set ; }
public string Framework { get ; set ; }
public string Runtime { get ; set ; }
2015-12-21 12:23:05 -08:00
public bool NativeSubdirectories { get ; set ; }
2015-12-10 23:00:35 -08:00
public NuGetFramework NugetFramework { get ; set ; }
2016-03-09 11:36:16 -08:00
public IList < ProjectContext > ProjectContexts { get ; set ; }
2016-02-18 01:09:23 -08:00
public string VersionSuffix { get ; set ; }
2015-12-10 23:00:35 -08:00
public int NumberOfProjects { get ; private set ; }
public int NumberOfPublishedProjects { get ; private set ; }
2016-03-29 14:15:26 -07:00
public bool ShouldBuild { get ; set ; }
2015-12-10 23:00:35 -08:00
public bool TryPrepareForPublish ( )
{
if ( Framework ! = null )
{
NugetFramework = NuGetFramework . Parse ( Framework ) ;
if ( NugetFramework . IsUnsupported )
{
Reporter . Output . WriteLine ( $"Unsupported framework {Framework}." . Red ( ) ) ;
return false ;
}
}
2016-03-09 11:36:16 -08:00
ProjectContexts = SelectContexts ( ProjectPath , NugetFramework , Runtime ) . ToList ( ) ;
2015-12-18 16:39:43 -08:00
if ( ! ProjectContexts . Any ( ) )
2015-12-10 23:00:35 -08:00
{
2016-03-09 11:36:16 -08:00
string errMsg = $"'{ProjectPath}' cannot be published for '{Framework ?? " < no framework provided > "}' '{Runtime ?? " < no runtime provided > "}'" ;
2015-12-10 23:00:35 -08:00
Reporter . Output . WriteLine ( errMsg . Red ( ) ) ;
return false ;
}
return true ;
}
public void PublishAllProjects ( )
{
NumberOfPublishedProjects = 0 ;
NumberOfProjects = 0 ;
2016-01-11 13:40:47 -08:00
2015-12-10 23:00:35 -08:00
foreach ( var project in ProjectContexts )
{
2016-02-03 10:57:25 -08:00
if ( PublishProjectContext ( project , BuildBasePath , OutputPath , Configuration , NativeSubdirectories ) )
2015-12-10 23:00:35 -08:00
{
NumberOfPublishedProjects + + ;
}
NumberOfProjects + + ;
}
}
/// <summary>
2016-03-01 17:35:32 -06:00
/// Publish the project for given 'framework (ex - netstandardapp1.5)' and 'runtimeID (ex - win7-x64)'
2015-12-10 23:00:35 -08:00
/// </summary>
/// <param name="context">project that is to be published</param>
2016-01-20 15:41:46 -08:00
/// <param name="baseOutputPath">Location of published files</param>
2015-12-10 23:00:35 -08:00
/// <param name="configuration">Debug or Release</param>
2016-01-20 15:41:46 -08:00
/// <param name="nativeSubdirectories"></param>
2015-12-10 23:00:35 -08:00
/// <returns>Return 0 if successful else return non-zero</returns>
2016-02-18 01:09:23 -08:00
private bool PublishProjectContext ( ProjectContext context , string buildBasePath , string outputPath , string configuration , bool nativeSubdirectories )
2015-12-10 23:00:35 -08:00
{
2016-03-09 11:36:16 -08:00
var target = context . TargetFramework . DotNetFrameworkName ;
if ( ! string . IsNullOrEmpty ( context . RuntimeIdentifier ) )
{
target = $"{target}/{context.RuntimeIdentifier}" ;
}
Reporter . Output . WriteLine ( $"Publishing {context.RootProject.Identity.Name.Yellow()} for {target.Yellow()}" ) ;
2015-12-10 23:00:35 -08:00
var options = context . ProjectFile . GetCompilerOptions ( context . TargetFramework , configuration ) ;
2016-02-17 10:08:27 -08:00
var outputPaths = context . GetOutputPaths ( configuration , buildBasePath , outputPath ) ;
2016-02-18 01:09:23 -08:00
2016-01-26 06:39:13 -08:00
if ( string . IsNullOrEmpty ( outputPath ) )
{
2016-02-17 10:08:27 -08:00
outputPath = Path . Combine ( outputPaths . RuntimeOutputPath , PublishSubfolderName ) ;
2016-01-26 06:39:13 -08:00
}
2015-12-10 23:00:35 -08:00
2016-01-11 13:40:47 -08:00
var contextVariables = new Dictionary < string , string >
{
{ "publish:ProjectPath" , context . ProjectDirectory } ,
{ "publish:Configuration" , configuration } ,
2016-01-24 20:28:52 -08:00
{ "publish:OutputPath" , outputPath } ,
2016-02-09 15:30:04 -08:00
{ "publish:TargetFramework" , context . TargetFramework . GetShortFolderName ( ) } ,
{ "publish:FullTargetFramework" , context . TargetFramework . DotNetFrameworkName } ,
2016-01-11 13:40:47 -08:00
{ "publish:Runtime" , context . RuntimeIdentifier } ,
} ;
RunScripts ( context , ScriptNames . PrePublish , contextVariables ) ;
2016-01-26 06:39:13 -08:00
if ( ! Directory . Exists ( outputPath ) )
2015-12-10 23:00:35 -08:00
{
2016-01-26 06:39:13 -08:00
Directory . CreateDirectory ( outputPath ) ;
2015-12-10 23:00:35 -08:00
}
// Compile the project (and transitively, all it's dependencies)
2016-03-29 14:15:26 -07:00
if ( ShouldBuild & & ! InvokeBuildOnProject ( context , buildBasePath , configuration ) )
2015-12-10 23:00:35 -08:00
{
return false ;
}
// Use a library exporter to collect publish assets
var exporter = context . CreateExporter ( configuration ) ;
2016-03-17 11:56:57 -07:00
var isPortable = string . IsNullOrEmpty ( context . RuntimeIdentifier ) ;
2016-03-25 15:40:16 -07:00
// Collect all exports and organize them
var exports = exporter . GetAllExports ( )
. Where ( e = > e . Library . Identity . Type . Equals ( LibraryType . Package ) )
. ToDictionary ( e = > e . Library . Identity . Name ) ;
var collectExclusionList = isPortable ? GetExclusionList ( context , exports ) : new HashSet < string > ( ) ;
foreach ( var export in exporter . GetAllExports ( ) . Where ( e = > ! collectExclusionList . Contains ( e . Library . Identity . Name ) ) )
2015-12-10 23:00:35 -08:00
{
Reporter . Verbose . WriteLine ( $"Publishing {export.Library.Identity.ToString().Green().Bold()} ..." ) ;
2016-03-17 11:56:57 -07:00
PublishAssetGroups ( export . RuntimeAssemblyGroups , outputPath , nativeSubdirectories : false , includeRuntimeGroups : isPortable ) ;
PublishAssetGroups ( export . NativeLibraryGroups , outputPath , nativeSubdirectories , includeRuntimeGroups : isPortable ) ;
2016-02-17 10:08:27 -08:00
export . RuntimeAssets . StructuredCopyTo ( outputPath , outputPaths . IntermediateOutputDirectoryPath ) ;
2016-01-21 16:40:33 -08:00
if ( options . PreserveCompilationContext . GetValueOrDefault ( ) )
{
PublishRefs ( export , outputPath ) ;
}
2015-12-10 23:00:35 -08:00
}
2016-03-09 11:36:16 -08:00
if ( context . ProjectFile . HasRuntimeOutput ( configuration ) & & ! context . TargetFramework . IsDesktop ( ) )
{
// Get the output paths used by the call to `dotnet build` above (since we didn't pass `--output`, they will be different from
// our current output paths)
var buildOutputPaths = context . GetOutputPaths ( configuration , buildBasePath ) ;
PublishFiles (
new [ ] {
buildOutputPaths . RuntimeFiles . DepsJson ,
buildOutputPaths . RuntimeFiles . RuntimeConfigJson
} ,
outputPath ) ;
}
2016-02-02 10:50:59 -08:00
var contentFiles = new ContentFiles ( context ) ;
contentFiles . StructuredCopyTo ( outputPath ) ;
2016-01-08 16:20:39 -08:00
2015-12-10 23:00:35 -08:00
// Publish a host if this is an application
2016-03-09 11:36:16 -08:00
if ( options . EmitEntryPoint . GetValueOrDefault ( ) & & ! string . IsNullOrEmpty ( context . RuntimeIdentifier ) )
2015-12-10 23:00:35 -08:00
{
2016-03-09 11:36:16 -08:00
Reporter . Verbose . WriteLine ( $"Copying native host to output to create fully standalone output." ) ;
2016-03-17 13:46:46 -07:00
PublishHost ( context , outputPath , options ) ;
2015-12-10 23:00:35 -08:00
}
2016-01-11 13:40:47 -08:00
RunScripts ( context , ScriptNames . PostPublish , contextVariables ) ;
2015-12-10 23:00:35 -08:00
Reporter . Output . WriteLine ( $"Published to {outputPath}" . Green ( ) . Bold ( ) ) ;
2016-01-11 13:40:47 -08:00
2015-12-10 23:00:35 -08:00
return true ;
}
2016-03-30 11:09:28 -07:00
private bool InvokeBuildOnProject ( ProjectContext context , string buildBasePath , string configuration )
2016-03-29 14:15:26 -07:00
{
var args = new List < string > ( )
{
"--framework" ,
$"{context.TargetFramework.DotNetFrameworkName}" ,
"--configuration" ,
configuration ,
context . ProjectFile . ProjectDirectory
} ;
if ( ! string . IsNullOrEmpty ( context . RuntimeIdentifier ) )
{
args . Insert ( 0 , context . RuntimeIdentifier ) ;
args . Insert ( 0 , "--runtime" ) ;
}
if ( ! string . IsNullOrEmpty ( VersionSuffix ) )
{
args . Add ( "--version-suffix" ) ;
args . Add ( VersionSuffix ) ;
}
if ( ! string . IsNullOrEmpty ( buildBasePath ) )
{
args . Add ( "--build-base-path" ) ;
args . Add ( buildBasePath ) ;
}
var result = Build . BuildCommand . Run ( args . ToArray ( ) ) ;
return result = = 0 ;
}
2016-03-25 15:40:16 -07:00
private HashSet < string > GetExclusionList ( ProjectContext context , Dictionary < string , LibraryExport > exports )
{
var exclusionList = new HashSet < string > ( ) ;
var redistPackages = context . RootProject . Dependencies
. Where ( r = > r . Type . Equals ( LibraryDependencyType . Platform ) )
. ToList ( ) ;
if ( redistPackages . Count = = 0 )
{
return exclusionList ;
}
else if ( redistPackages . Count > 1 )
{
throw new InvalidOperationException ( "Multiple packages with type: \"platform\" were specified!" ) ;
}
var redistExport = exports [ redistPackages [ 0 ] . Name ] ;
exclusionList . Add ( redistExport . Library . Identity . Name ) ;
CollectDependencies ( exports , redistExport . Library . Dependencies , exclusionList ) ;
return exclusionList ;
}
private void CollectDependencies ( Dictionary < string , LibraryExport > exports , IEnumerable < LibraryRange > dependencies , HashSet < string > exclusionList )
{
foreach ( var dependency in dependencies )
{
var export = exports [ dependency . Name ] ;
if ( export . Library . Identity . Version . Equals ( dependency . VersionRange . MinVersion ) )
{
exclusionList . Add ( export . Library . Identity . Name ) ;
CollectDependencies ( exports , export . Library . Dependencies , exclusionList ) ;
}
}
}
2016-01-21 16:40:33 -08:00
private static void PublishRefs ( LibraryExport export , string outputPath )
{
var refsPath = Path . Combine ( outputPath , "refs" ) ;
if ( ! Directory . Exists ( refsPath ) )
{
Directory . CreateDirectory ( refsPath ) ;
}
// Do not copy compilation assembly if it's in runtime assemblies
2016-03-17 11:56:57 -07:00
var runtimeAssemblies = new HashSet < LibraryAsset > ( export . RuntimeAssemblyGroups . GetDefaultAssets ( ) ) ;
2016-01-21 16:40:33 -08:00
foreach ( var compilationAssembly in export . CompilationAssemblies )
{
if ( ! runtimeAssemblies . Contains ( compilationAssembly ) )
{
var destFileName = Path . Combine ( refsPath , Path . GetFileName ( compilationAssembly . ResolvedPath ) ) ;
File . Copy ( compilationAssembly . ResolvedPath , destFileName , overwrite : true ) ;
}
}
}
2016-03-17 13:46:46 -07:00
private static int PublishHost ( ProjectContext context , string outputPath , CommonCompilerOptions compilationOptions )
2015-12-10 23:00:35 -08:00
{
if ( context . TargetFramework . IsDesktop ( ) )
{
return 0 ;
}
2016-01-09 23:33:22 -08:00
foreach ( var binaryName in Constants . HostBinaryNames )
2015-12-10 23:00:35 -08:00
{
2016-01-09 23:33:22 -08:00
var hostBinaryPath = Path . Combine ( AppContext . BaseDirectory , binaryName ) ;
if ( ! File . Exists ( hostBinaryPath ) )
{
Reporter . Error . WriteLine ( $"Cannot find {binaryName} in the dotnet directory." . Red ( ) ) ;
return 1 ;
}
2015-12-10 23:00:35 -08:00
2016-03-17 13:46:46 -07:00
var outputBinaryName = binaryName . Equals ( Constants . HostExecutableName )
2016-03-17 16:52:50 -07:00
? compilationOptions . OutputName + Constants . ExeSuffix
2016-03-17 13:46:46 -07:00
: binaryName ;
2016-01-09 23:33:22 -08:00
var outputBinaryPath = Path . Combine ( outputPath , outputBinaryName ) ;
2015-12-10 23:00:35 -08:00
2016-01-09 23:33:22 -08:00
File . Copy ( hostBinaryPath , outputBinaryPath , overwrite : true ) ;
}
2015-12-10 23:00:35 -08:00
return 0 ;
}
2016-02-03 10:57:25 -08:00
2016-01-27 20:35:43 -08:00
private static void PublishFiles ( IEnumerable < string > files , string outputPath )
{
foreach ( var file in files )
{
var targetPath = Path . Combine ( outputPath , Path . GetFileName ( file ) ) ;
File . Copy ( file , targetPath , overwrite : true ) ;
}
}
2015-12-10 23:00:35 -08:00
2016-03-17 11:56:57 -07:00
private void PublishAssetGroups ( IEnumerable < LibraryAssetGroup > groups , string outputPath , bool nativeSubdirectories , bool includeRuntimeGroups )
2015-12-10 23:00:35 -08:00
{
2016-03-17 11:56:57 -07:00
foreach ( var group in groups . Where ( g = > includeRuntimeGroups | | string . IsNullOrEmpty ( g . Runtime ) ) )
2015-12-10 23:00:35 -08:00
{
2016-03-17 11:56:57 -07:00
foreach ( var file in group . Assets )
2016-03-09 11:36:16 -08:00
{
2016-03-17 11:56:57 -07:00
var destinationDirectory = DetermineFileDestinationDirectory ( file , outputPath , nativeSubdirectories ) ;
2016-03-09 11:36:16 -08:00
2016-03-17 11:56:57 -07:00
if ( ! string . IsNullOrEmpty ( group . Runtime ) )
{
destinationDirectory = Path . Combine ( destinationDirectory , Path . GetDirectoryName ( file . RelativePath ) ) ;
}
2016-02-18 01:09:23 -08:00
2016-03-17 11:56:57 -07:00
if ( ! Directory . Exists ( destinationDirectory ) )
{
Directory . CreateDirectory ( destinationDirectory ) ;
}
File . Copy ( file . ResolvedPath , Path . Combine ( destinationDirectory , file . FileName ) , overwrite : true ) ;
}
2015-12-21 12:23:05 -08:00
}
}
private static string DetermineFileDestinationDirectory ( LibraryAsset file , string outputPath , bool nativeSubdirectories )
{
var destinationDirectory = outputPath ;
if ( nativeSubdirectories )
{
destinationDirectory = Path . Combine ( outputPath , GetNativeRelativeSubdirectory ( file . RelativePath ) ) ;
2015-12-10 23:00:35 -08:00
}
2015-12-21 12:23:05 -08:00
return destinationDirectory ;
}
private static string GetNativeRelativeSubdirectory ( string filepath )
{
string directoryPath = Path . GetDirectoryName ( filepath ) ;
string [ ] parts = directoryPath . Split ( new string [ ] { "native" } , 2 , StringSplitOptions . None ) ;
if ( parts . Length ! = 2 )
{
throw new Exception ( "Unrecognized Native Directory Format: " + filepath ) ;
}
string candidate = parts [ 1 ] ;
candidate = candidate . TrimStart ( new char [ ] { '/' , '\\' } ) ;
return candidate ;
2015-12-10 23:00:35 -08:00
}
2015-12-18 16:39:43 -08:00
2016-03-09 11:36:16 -08:00
private IEnumerable < ProjectContext > SelectContexts ( string projectPath , NuGetFramework framework , string runtime )
2015-12-18 16:39:43 -08:00
{
2016-03-09 11:36:16 -08:00
var allContexts = ProjectContext . CreateContextForEachTarget ( projectPath ) . ToList ( ) ;
var frameworks = framework = = null ?
allContexts . Select ( c = > c . TargetFramework ) . Distinct ( ) . ToArray ( ) :
new [ ] { framework } ;
2015-12-18 16:39:43 -08:00
if ( string . IsNullOrEmpty ( runtime ) )
{
2016-03-09 11:36:16 -08:00
// For each framework, find the best matching RID item
var candidates = PlatformServices . Default . Runtime . GetAllCandidateRuntimeIdentifiers ( ) ;
return frameworks . Select ( f = > FindBestTarget ( f , allContexts , candidates ) ) ;
2015-12-18 16:39:43 -08:00
}
else
{
2016-03-09 11:36:16 -08:00
return frameworks . SelectMany ( f = > allContexts . Where ( c = >
Equals ( c . TargetFramework , f ) & &
string . Equals ( c . RuntimeIdentifier , runtime , StringComparison . Ordinal ) ) ) ;
}
}
private ProjectContext FindBestTarget ( NuGetFramework f , List < ProjectContext > allContexts , IEnumerable < string > candidates )
{
foreach ( var candidate in candidates )
{
var target = allContexts . FirstOrDefault ( c = >
Equals ( c . TargetFramework , f ) & &
string . Equals ( c . RuntimeIdentifier , candidate , StringComparison . Ordinal ) ) ;
if ( target ! = null )
{
return target ;
}
2015-12-18 16:39:43 -08:00
}
2016-03-09 11:36:16 -08:00
// No RID-specific target found, use the RID-less target and publish portable
2016-03-17 13:46:46 -07:00
return allContexts . FirstOrDefault ( c = >
Equals ( c . TargetFramework , f ) & &
2016-03-09 11:36:16 -08:00
string . IsNullOrEmpty ( c . RuntimeIdentifier ) ) ;
2015-12-18 16:39:43 -08:00
}
/// <summary>
/// Return the matching framework/runtime ProjectContext.
/// If 'framework' or 'runtimeIdentifier' is null or empty then it matches with any.
/// </summary>
private static IEnumerable < ProjectContext > GetMatchingProjectContexts ( IEnumerable < ProjectContext > contexts , NuGetFramework framework , string runtimeIdentifier )
{
foreach ( var context in contexts )
{
if ( context . TargetFramework = = null | | string . IsNullOrEmpty ( context . RuntimeIdentifier ) )
{
continue ;
}
if ( string . IsNullOrEmpty ( runtimeIdentifier ) | | string . Equals ( runtimeIdentifier , context . RuntimeIdentifier , StringComparison . OrdinalIgnoreCase ) )
{
if ( framework = = null | | framework . Equals ( context . TargetFramework ) )
{
yield return context ;
}
}
}
}
2016-01-08 16:20:39 -08:00
private static void CopyContents ( ProjectContext context , string outputPath )
{
var contentFiles = context . ProjectFile . Files . GetContentFiles ( ) ;
Copy ( contentFiles , context . ProjectDirectory , outputPath ) ;
}
private static void Copy ( IEnumerable < string > contentFiles , string sourceDirectory , string targetDirectory )
{
if ( contentFiles = = null )
{
throw new ArgumentNullException ( nameof ( contentFiles ) ) ;
}
sourceDirectory = PathUtility . EnsureTrailingSlash ( sourceDirectory ) ;
targetDirectory = PathUtility . EnsureTrailingSlash ( targetDirectory ) ;
foreach ( var contentFilePath in contentFiles )
{
Reporter . Verbose . WriteLine ( $"Publishing {contentFilePath.Green().Bold()} ..." ) ;
var fileName = Path . GetFileName ( contentFilePath ) ;
var targetFilePath = contentFilePath . Replace ( sourceDirectory , targetDirectory ) ;
var targetFileParentFolder = Path . GetDirectoryName ( targetFilePath ) ;
// Create directory before copying a file
if ( ! Directory . Exists ( targetFileParentFolder ) )
{
Directory . CreateDirectory ( targetFileParentFolder ) ;
}
File . Copy (
contentFilePath ,
targetFilePath ,
overwrite : true ) ;
// clear read-only bit if set
var fileAttributes = File . GetAttributes ( targetFilePath ) ;
if ( ( fileAttributes & FileAttributes . ReadOnly ) = = FileAttributes . ReadOnly )
{
File . SetAttributes ( targetFilePath , fileAttributes & ~ FileAttributes . ReadOnly ) ;
}
}
}
2016-01-11 13:40:47 -08:00
private static void RunScripts ( ProjectContext context , string name , Dictionary < string , string > contextVariables )
{
foreach ( var script in context . ProjectFile . Scripts . GetOrEmpty ( name ) )
{
ScriptExecutor . CreateCommandForScript ( context . ProjectFile , script , contextVariables )
. ForwardStdErr ( )
. ForwardStdOut ( )
. Execute ( ) ;
}
}
2015-12-10 23:00:35 -08:00
}
}