dotnet 职业技术学院

博客

dotnet 职业技术学院

编写 MSBuild 内联编译任务(Task)用于获取当前编译环境下的所有编译目标(Target)

dotnet 职业技术学院 发布于 2019-03-01

我之前写过一些改变 MSBuild 编译过程的一些博客,包括利用 Microsoft.NET.Sdk 中各种自带的 Task 来执行各种各样的编译任务。更复杂的任务难以直接利用自带的 Task 实现,需要自己写 Task。

本文将编写一个内联的编译任务,获取当前编译环境下的所有编译目标(Target)。获取所有的这些 Target 对我们调试一些与 MSBuild 或编译相关的问题时可能带来一些帮助。


编写纯 C# 版本编译任务获取所有编译目标(Target)的代码是这样的:

using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;

public class WalterlvGetAllTargets : Task
{
    public string ProjectFile { get; set; }

    public ITaskItem[] WalterlvTargets { get; set; }

    public override bool Execute()
    {
        var project = new Project(ProjectFile);

        var taskItems = new List<ITaskItem>(project.Targets.Count);
        foreach (KeyValuePair<string, ProjectTargetInstance> pair in project.Targets)
        {
            var target = pair.Value;
            var metadata = new Dictionary<string, string>
            {
                { "Condition", target.Condition },
                { "Inputs", target.Inputs },
                { "Outputs", target.Outputs },
                { "DependsOnTargets", target.DependsOnTargets }
            };
            taskItems.Add(new TaskItem(pair.Key, metadata));
        }

        WalterlvTargets = taskItems.ToArray();

        return true;
    }
}

那么转换成内联版本下面这样。为了方便验证,我直接把完整的 csproj 文件贴出来了。如果你希望在你的项目中去使用,可以只复制 UsingTask 和 Target 两个部分。

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net472</TargetFramework>
    </PropertyGroup>

    <UsingTask TaskName="WalterlvGetAllTargets" TaskFactory="CodeTaskFactory"
               AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
        <ParameterGroup>
            <!-- 内联 C# 代码的输入参数(Task 的输入属性),相当于 public string ProjectFile { get; set; } -->
            <ProjectFile ParameterType="System.String" Required="true"/>
            <!-- 内联 C# 代码的输出参数(Task 的输入属性),相当于 public ITaskItem[] WalterlvTargets { get; set; } -->
            <WalterlvTargets ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true"/>
        </ParameterGroup>
        <Task>
            <!-- 引用程序集。 -->
            <Reference Include="System.Xml"/>
            <Reference Include="Microsoft.Build"/>
            <Reference Include="Microsoft.Build.Framework"/>
            <!-- 编写 C# 代码所用到的 using。 -->
            <Using Namespace="Microsoft.Build.Evaluation"/>
            <Using Namespace="Microsoft.Build.Execution"/>
            <Using Namespace="Microsoft.Build.Utilities"/>
            <Using Namespace="Microsoft.Build.Framework"/>
            <!-- 开始插入 C# 代码。 -->
            <Code Type="Fragment" Language="cs">
        <![CDATA[
            var project = new Project(ProjectFile);

            var taskItems = new List<ITaskItem>(project.Targets.Count);
            foreach (KeyValuePair<string, ProjectTargetInstance> pair in project.Targets)
            {
                var target = pair.Value;
                var metadata = new Dictionary<string, string>
                {
                    { "Condition", target.Condition },
                    { "Inputs", target.Inputs },
                    { "Outputs", target.Outputs },
                    { "DependsOnTargets", target.DependsOnTargets }
                };
                taskItems.Add(new TaskItem(pair.Key, metadata));
            }

            WalterlvTargets = taskItems.ToArray();
        ]]>
            </Code>
        </Task>
    </UsingTask>

    <Target Name="WalterlvOutputAllTargets" AfterTargets="Build">
        <!-- 执行刚刚写的内联 Task,然后获取它的输出参数 WalterlvTargets 并填充到 TargetItems 集合中。 -->
        <WalterlvGetAllTargets ProjectFile="$(MSBuildProjectFile)">
            <Output ItemName="TargetItems" TaskParameter="WalterlvTargets"/>
        </WalterlvGetAllTargets>
        <!-- 用一个 Message 输出刚刚生成的 TargetItems 集合中每一项的 Identity 属性(集合中每一项都会输出。) -->
        <Message Text="输出的 Target:%(TargetItems.Identity)"/>
    </Target>
<Project>

现在使用 msbuild 命令进行编译,我们将看到所有 Target 的输出:

输出的所有 Target

WalterlvOutputAllTargets:
  输出的 Target:OutputAll
  输出的 Target:_CheckForUnsupportedTargetFramework
  输出的 Target:_CollectTargetFrameworkForTelemetry
  输出的 Target:_CheckForUnsupportedNETCoreVersion
  输出的 Target:_CheckForUnsupportedNETStandardVersion
  输出的 Target:_CheckForUnsupportedAppHostUsage
  输出的 Target:_CheckForMismatchingPlatform
  输出的 Target:_CheckForNETCoreSdkIsPreview
  输出的 Target:AdjustDefaultPlatformTargetForNetFrameworkExeWithNoNativeCopyLocalItems
  输出的 Target:CreateManifestResourceNames
  输出的 Target:ResolveCodeAnalysisRuleSet
  输出的 Target:XamlPreCompile
  输出的 Target:ShimReferencePathsWhenCommonTargetsDoesNotUnderstandReferenceAssemblies
  输出的 Target:_BeforeVBCSCoreCompile
  输出的 Target:InitializeSourceRootMappedPaths
  输出的 Target:_InitializeSourceRootMappedPathsFromSourceControl
  输出的 Target:_SetPathMapFromSourceRoots
  输出的 Target:CoreCompile
  输出的 Target:ResolvePackageDependenciesDesignTime
  输出的 Target:CollectSDKReferencesDesignTime
  输出的 Target:CollectResolvedSDKReferencesDesignTime
  输出的 Target:CollectPackageReferences
  输出的 Target:_CheckCompileDesignTimePrerequisite
  输出的 Target:CollectAnalyzersDesignTime
  输出的 Target:CollectResolvedCompilationReferencesDesignTime
  输出的 Target:CollectUpToDateCheckInputDesignTime
  输出的 Target:CollectUpToDateCheckOutputDesignTime
  输出的 Target:CollectUpToDateCheckBuiltDesignTime
  输出的 Target:CompileDesignTime
  输出的 Target:_FixVCLibs120References
  输出的 Target:_AddVCLibs140UniversalCrtDebugReference
  输出的 Target:InitializeSourceControlInformation
  输出的 Target:_CheckForInvalidConfigurationAndPlatform
  输出的 Target:Build
  输出的 Target:BeforeBuild
  输出的 Target:AfterBuild
  输出的 Target:CoreBuild
  输出的 Target:Rebuild
  输出的 Target:BeforeRebuild
  输出的 Target:AfterRebuild
  输出的 Target:BuildGenerateSources
  输出的 Target:BuildGenerateSourcesTraverse
  输出的 Target:BuildCompile
  输出的 Target:BuildCompileTraverse
  输出的 Target:BuildLink
  输出的 Target:BuildLinkTraverse
  输出的 Target:CopyRunEnvironmentFiles
  输出的 Target:Run
  输出的 Target:BuildOnlySettings
  输出的 Target:PrepareForBuild
  输出的 Target:GetFrameworkPaths
  输出的 Target:GetReferenceAssemblyPaths
  输出的 Target:GetTargetFrameworkMoniker
  输出的 Target:GetTargetFrameworkMonikerDisplayName
  输出的 Target:GetTargetFrameworkDirectories
  输出的 Target:AssignLinkMetadata
  输出的 Target:PreBuildEvent
  输出的 Target:UnmanagedUnregistration
  输出的 Target:GetTargetFrameworkVersion
  输出的 Target:ResolveReferences
  输出的 Target:BeforeResolveReferences
  输出的 Target:AfterResolveReferences
  输出的 Target:AssignProjectConfiguration
  输出的 Target:_SplitProjectReferencesByFileExistence
  输出的 Target:_GetProjectReferenceTargetFrameworkProperties
  输出的 Target:GetTargetFrameworks
  输出的 Target:GetTargetFrameworkProperties
  输出的 Target:PrepareProjectReferences
  输出的 Target:ResolveProjectReferences
  输出的 Target:ResolveProjectReferencesDesignTime
  输出的 Target:ExpandSDKReferencesDesignTime
  输出的 Target:GetTargetPath
  输出的 Target:GetTargetPathWithTargetPlatformMoniker
  输出的 Target:GetNativeManifest
  输出的 Target:ResolveNativeReferences
  输出的 Target:ResolveAssemblyReferences
  输出的 Target:FindReferenceAssembliesForReferences
  输出的 Target:GenerateBindingRedirects
  输出的 Target:GenerateBindingRedirectsUpdateAppConfig
  输出的 Target:GetInstalledSDKLocations
  输出的 Target:ResolveSDKReferences
  输出的 Target:ResolveSDKReferencesDesignTime
  输出的 Target:FindInvalidProjectReferences
  输出的 Target:GetReferenceTargetPlatformMonikers
  输出的 Target:ExpandSDKReferences
  输出的 Target:ExportWindowsMDFile
  输出的 Target:ResolveAssemblyReferencesDesignTime
  输出的 Target:DesignTimeResolveAssemblyReferences
  输出的 Target:ResolveComReferences
  输出的 Target:ResolveComReferencesDesignTime
  输出的 Target:PrepareResources
  输出的 Target:PrepareResourceNames
  输出的 Target:AssignTargetPaths
  输出的 Target:GetItemTargetPaths
  输出的 Target:SplitResourcesByCulture
  输出的 Target:CreateCustomManifestResourceNames
  输出的 Target:ResGen
  输出的 Target:BeforeResGen
  输出的 Target:AfterResGen
  输出的 Target:CoreResGen
  输出的 Target:CompileLicxFiles
  输出的 Target:ResolveKeySource
  输出的 Target:Compile
  输出的 Target:_GenerateCompileInputs
  输出的 Target:GenerateTargetFrameworkMonikerAttribute
  输出的 Target:GenerateAdditionalSources
  输出的 Target:BeforeCompile
  输出的 Target:AfterCompile
  输出的 Target:_TimeStampBeforeCompile
  输出的 Target:_GenerateCompileDependencyCache
  输出的 Target:_TimeStampAfterCompile
  输出的 Target:_ComputeNonExistentFileProperty
  输出的 Target:GenerateSerializationAssemblies
  输出的 Target:CreateSatelliteAssemblies
  输出的 Target:_GenerateSatelliteAssemblyInputs
  输出的 Target:GenerateSatelliteAssemblies
  输出的 Target:ComputeIntermediateSatelliteAssemblies
  输出的 Target:SetWin32ManifestProperties
  输出的 Target:_SetExternalWin32ManifestProperties
  输出的 Target:_SetEmbeddedWin32ManifestProperties
  输出的 Target:_GenerateResolvedDeploymentManifestEntryPoint
  输出的 Target:GenerateManifests
  输出的 Target:GenerateApplicationManifest
  输出的 Target:_DeploymentComputeNativeManifestInfo
  输出的 Target:_DeploymentComputeClickOnceManifestInfo
  输出的 Target:_DeploymentGenerateTrustInfo
  输出的 Target:GenerateDeploymentManifest
  输出的 Target:PrepareForRun
  输出的 Target:CopyFilesToOutputDirectory
  输出的 Target:_CopyFilesMarkedCopyLocal
  输出的 Target:_CopySourceItemsToOutputDirectory
  输出的 Target:GetCopyToOutputDirectoryItems
  输出的 Target:GetCopyToPublishDirectoryItems
  输出的 Target:_CopyOutOfDateSourceItemsToOutputDirectory
  输出的 Target:_CopyOutOfDateSourceItemsToOutputDirectoryAlways
  输出的 Target:_CopyAppConfigFile
  输出的 Target:_CopyManifestFiles
  输出的 Target:_CheckForCompileOutputs
  输出的 Target:_SGenCheckForOutputs
  输出的 Target:UnmanagedRegistration
  输出的 Target:IncrementalClean
  输出的 Target:_CleanGetCurrentAndPriorFileWrites
  输出的 Target:Clean
  输出的 Target:BeforeClean
  输出的 Target:AfterClean
  输出的 Target:CleanReferencedProjects
  输出的 Target:CoreClean
  输出的 Target:_CleanRecordFileWrites
  输出的 Target:CleanPublishFolder
  输出的 Target:PostBuildEvent
  输出的 Target:Publish
  输出的 Target:_DeploymentUnpublishable
  输出的 Target:SetGenerateManifests
  输出的 Target:PublishOnly
  输出的 Target:BeforePublish
  输出的 Target:AfterPublish
  输出的 Target:PublishBuild
  输出的 Target:_CopyFilesToPublishFolder
  输出的 Target:_DeploymentGenerateBootstrapper
  输出的 Target:_DeploymentSignClickOnceDeployment
  输出的 Target:AllProjectOutputGroups
  输出的 Target:BuiltProjectOutputGroup
  输出的 Target:DebugSymbolsProjectOutputGroup
  输出的 Target:DocumentationProjectOutputGroup
  输出的 Target:SatelliteDllsProjectOutputGroup
  输出的 Target:SourceFilesProjectOutputGroup
  输出的 Target:GetCompile
  输出的 Target:ContentFilesProjectOutputGroup
  输出的 Target:SGenFilesOutputGroup
  输出的 Target:GetResolvedSDKReferences
  输出的 Target:CollectReferencedNuGetPackages
  输出的 Target:PriFilesOutputGroup
  输出的 Target:SDKRedistOutputGroup
  输出的 Target:AllProjectOutputGroupsDependencies
  输出的 Target:BuiltProjectOutputGroupDependencies
  输出的 Target:DebugSymbolsProjectOutputGroupDependencies
  输出的 Target:SatelliteDllsProjectOutputGroupDependencies
  输出的 Target:DocumentationProjectOutputGroupDependencies
  输出的 Target:SGenFilesOutputGroupDependencies
  输出的 Target:ReferenceCopyLocalPathsOutputGroup
  输出的 Target:SetCABuildNativeEnvironmentVariables
  输出的 Target:RunCodeAnalysis
  输出的 Target:RunNativeCodeAnalysis
  输出的 Target:RunSelectedFileNativeCodeAnalysis
  输出的 Target:RunMergeNativeCodeAnalysis
  输出的 Target:ImplicitlyExpandDesignTimeFacades
  输出的 Target:GetWinFXPath
  输出的 Target:DesignTimeMarkupCompilation
  输出的 Target:PrepareResourcesForSatelliteAssemblies
  输出的 Target:_AfterCompileWinFXInternal
  输出的 Target:AfterCompileWinFX
  输出的 Target:AfterMarkupCompilePass1
  输出的 Target:AfterMarkupCompilePass2
  输出的 Target:MarkupCompilePass1
  输出的 Target:MarkupCompilePass2
  输出的 Target:_CompileTemporaryAssembly
  输出的 Target:MarkupCompilePass2ForMainAssembly
  输出的 Target:GenerateTemporaryTargetAssembly
  输出的 Target:CleanupTemporaryTargetAssembly
  输出的 Target:AddIntermediateAssemblyToReferenceList
  输出的 Target:SatelliteOnlyMarkupCompilePass2
  输出的 Target:HostInBrowserValidation
  输出的 Target:SplashScreenValidation
  输出的 Target:ResignApplicationManifest
  输出的 Target:SignDeploymentManifest
  输出的 Target:FileClassification
  输出的 Target:MainResourcesGeneration
  输出的 Target:SatelliteResourceGeneration
  输出的 Target:GenerateResourceWithCultureItem
  输出的 Target:CheckUid
  输出的 Target:UpdateUid
  输出的 Target:RemoveUid
  输出的 Target:MergeLocalizationDirectives
  输出的 Target:AssignWinFXEmbeddedResource
  输出的 Target:EntityDeploy
  输出的 Target:EntityDeploySplit
  输出的 Target:EntityDeployNonEmbeddedResources
  输出的 Target:EntityDeployEmbeddedResources
  输出的 Target:EntityClean
  输出的 Target:EntityDeploySetLogicalNames
  输出的 Target:DesignTimeXamlMarkupCompilation
  输出的 Target:InProcessXamlMarkupCompilePass1
  输出的 Target:CleanInProcessXamlGeneratedFiles
  输出的 Target:XamlMarkupCompileReadGeneratedFileList
  输出的 Target:XamlMarkupCompilePass1
  输出的 Target:XamlMarkupCompileAddFilesGenerated
  输出的 Target:XamlMarkupCompileReadPass2Flag
  输出的 Target:XamlTemporaryAssemblyGeneration
  输出的 Target:CompileTemporaryAssembly
  输出的 Target:XamlMarkupCompilePass2
  输出的 Target:XamlMarkupCompileAddExtensionFilesGenerated
  输出的 Target:GetCopyToOutputDirectoryXamlAppDefs
  输出的 Target:ExpressionBuildExtension
  输出的 Target:ValidationExtension
  输出的 Target:GenerateCompiledExpressionsTempFile
  输出的 Target:AddDeferredValidationErrorsFileToFileWrites
  输出的 Target:ReportValidationBuildExtensionErrors
  输出的 Target:DeferredValidation
  输出的 Target:ResolveTestReferences
  输出的 Target:CleanAppxPackage
  输出的 Target:GetPackagingOutputs
  输出的 Target:Restore
  输出的 Target:GenerateRestoreGraphFile
  输出的 Target:_LoadRestoreGraphEntryPoints
  输出的 Target:_FilterRestoreGraphProjectInputItems
  输出的 Target:_GenerateRestoreGraph
  输出的 Target:_GenerateRestoreGraphProjectEntry
  输出的 Target:_GenerateRestoreSpecs
  输出的 Target:_GenerateDotnetCliToolReferenceSpecs
  输出的 Target:_GetProjectJsonPath
  输出的 Target:_GetRestoreProjectStyle
  输出的 Target:EnableIntermediateOutputPathMismatchWarning
  输出的 Target:_GetRestoreTargetFrameworksOutput
  输出的 Target:_GetRestoreTargetFrameworksAsItems
  输出的 Target:_GetRestoreSettings
  输出的 Target:_GetRestoreSettingsCurrentProject
  输出的 Target:_GetRestoreSettingsAllFrameworks
  输出的 Target:_GetRestoreSettingsPerFramework
  输出的 Target:_GenerateRestoreProjectSpec
  输出的 Target:_GenerateProjectRestoreGraph
  输出的 Target:_GenerateRestoreDependencies
  输出的 Target:_GenerateProjectRestoreGraphAllFrameworks
  输出的 Target:_GenerateProjectRestoreGraphCurrentProject
  输出的 Target:_GenerateProjectRestoreGraphPerFramework
  输出的 Target:_GenerateRestoreProjectPathItemsCurrentProject
  输出的 Target:_GenerateRestoreProjectPathItemsPerFramework
  输出的 Target:_GenerateRestoreProjectPathItems
  输出的 Target:_GenerateRestoreProjectPathItemsAllFrameworks
  输出的 Target:_GenerateRestoreProjectPathWalk
  输出的 Target:_GetAllRestoreProjectPathItems
  输出的 Target:_GetRestoreSettingsOverrides
  输出的 Target:_GetRestorePackagesPathOverride
  输出的 Target:_GetRestoreSourcesOverride
  输出的 Target:_GetRestoreFallbackFoldersOverride
  输出的 Target:_IsProjectRestoreSupported
  输出的 Target:DesktopBridgeCopyLocalOutputGroup
  输出的 Target:DesktopBridgeComFilesOutputGroup
  输出的 Target:GetDeployableContentReferenceOutputs
  输出的 Target:DockerResolveAppType
  输出的 Target:DockerUpdateComposeVsGeneratedFiles
  输出的 Target:DockerResolveTargetFramework
  输出的 Target:DockerComposeBuild
  输出的 Target:DockerPackageService
  输出的 Target:ImplicitlyExpandNETStandardFacades
  输出的 Target:_RemoveZipFileSuggestedRedirect
  输出的 Target:SetARM64AppxPackageInputsForInboxNetNative
  输出的 Target:_CleanMdbFiles
  输出的 Target:PreXsdCodeGen
  输出的 Target:XsdCodeGen
  输出的 Target:XsdResolveReferencePath
  输出的 Target:CleanXsdCodeGen
  输出的 Target:_SetTargetFrameworkMonikerAttribute
  输出的 Target:ResolvePackageDependenciesForBuild
  输出的 Target:RunResolvePackageDependencies
  输出的 Target:ResolvePackageAssets
  输出的 Target:FilterSatelliteResources
  输出的 Target:RunProduceContentAssets
  输出的 Target:ReportAssetsLogMessages
  输出的 Target:ResolveLockFileReferences
  输出的 Target:IncludeTransitiveProjectReferences
  输出的 Target:ResolveLockFileAnalyzers
  输出的 Target:_ComputeLockFileCopyLocal
  输出的 Target:ResolveLockFileCopyLocalProjectDeps
  输出的 Target:CheckForImplicitPackageReferenceOverrides
  输出的 Target:CheckForDuplicateItems
  输出的 Target:GenerateBuildDependencyFile
  输出的 Target:GenerateBuildRuntimeConfigurationFiles
  输出的 Target:AddRuntimeConfigFileToBuiltProjectOutputGroupOutput
  输出的 Target:_SdkBeforeClean
  输出的 Target:_SdkBeforeRebuild
  输出的 Target:_ComputeNETCoreBuildOutputFiles
  输出的 Target:_ComputeReferenceAssemblies
  输出的 Target:CoreGenerateSatelliteAssemblies
  输出的 Target:_GetAssemblyInfoFromTemplateFile
  输出的 Target:_DefaultMicrosoftNETPlatformLibrary
  输出的 Target:GetAllRuntimeIdentifiers
  输出的 Target:GenerateAssemblyInfo
  输出的 Target:AddSourceRevisionToInformationalVersion
  输出的 Target:GetAssemblyAttributes
  输出的 Target:CreateGeneratedAssemblyInfoInputsCacheFile
  输出的 Target:CoreGenerateAssemblyInfo
  输出的 Target:GetAssemblyVersion
  输出的 Target:ComposeStore
  输出的 Target:StoreWorkerMain
  输出的 Target:StoreWorkerMapper
  输出的 Target:StoreResolver
  输出的 Target:StoreWorkerPerformWork
  输出的 Target:StoreFinalizer
  输出的 Target:_CopyResolvedOptimizedFiles
  输出的 Target:PrepareForComposeStore
  输出的 Target:PrepforRestoreForComposeStore
  输出的 Target:RestoreForComposeStore
  输出的 Target:ComputeAndCopyFilesToStoreDirectory
  输出的 Target:CopyFilesToStoreDirectory
  输出的 Target:_CopyResolvedUnOptimizedFiles
  输出的 Target:_ComputeResolvedFilesToStoreTypes
  输出的 Target:_SplitResolvedFiles
  输出的 Target:_GetResolvedFilesToStore
  输出的 Target:ComputeFilesToStore
  输出的 Target:PrepRestoreForStoreProjects
  输出的 Target:PrepOptimizer
  输出的 Target:_RunOptimizer
  输出的 Target:RunCrossGen
  输出的 Target:_InitializeBasicProps
  输出的 Target:_GetCrossgenProps
  输出的 Target:_SetupStageForCrossgen
  输出的 Target:_RestoreCrossgen
  输出的 Target:_CheckForObsoleteDotNetCliToolReferences
  输出的 Target:_PublishBuildAlternative
  输出的 Target:_PublishNoBuildAlternative
  输出的 Target:_PreventProjectReferencesFromBuilding
  输出的 Target:PrepareForPublish
  输出的 Target:ComputeAndCopyFilesToPublishDirectory
  输出的 Target:CopyFilesToPublishDirectory
  输出的 Target:_CopyResolvedFilesToPublishPreserveNewest
  输出的 Target:_CopyResolvedFilesToPublishAlways
  输出的 Target:_ComputeResolvedFilesToPublishTypes
  输出的 Target:ComputeFilesToPublish
  输出的 Target:_ComputeNetPublishAssets
  输出的 Target:RunResolvePublishAssemblies
  输出的 Target:FilterPublishSatelliteResources
  输出的 Target:_ComputeCopyToPublishDirectoryItems
  输出的 Target:DefaultCopyToPublishDirectoryMetadata
  输出的 Target:GeneratePublishDependencyFile
  输出的 Target:_ComputeExcludeFromPublishPackageReferences
  输出的 Target:_ParseTargetManifestFiles
  输出的 Target:GeneratePublishRuntimeConfigurationFile
  输出的 Target:DeployAppHost
  输出的 Target:PackTool
  输出的 Target:GenerateToolsSettingsFileFromBuildProperty
  输出的 Target:ResolveApphostAsset
  输出的 Target:ComputeDependencyFileCompilerOptions
  输出的 Target:ComputeRefAssembliesToPublish
  输出的 Target:_CopyReferenceOnlyAssembliesForBuild
  输出的 Target:_HandlePackageFileConflicts
  输出的 Target:_HandlePublishFileConflicts
  输出的 Target:_GetOutputItemsFromPack
  输出的 Target:_GetTargetFrameworksOutput
  输出的 Target:_PackAsBuildAfterTarget
  输出的 Target:_CleanPackageFiles
  输出的 Target:_CalculateInputsOutputsForPack
  输出的 Target:Pack
  输出的 Target:_IntermediatePack
  输出的 Target:GenerateNuspec
  输出的 Target:_InitializeNuspecRepositoryInformationProperties
  输出的 Target:_LoadPackInputItems
  输出的 Target:_GetProjectReferenceVersions
  输出的 Target:_GetProjectVersion
  输出的 Target:_WalkEachTargetPerFramework
  输出的 Target:_GetFrameworksWithSuppressedDependencies
  输出的 Target:_GetFrameworkAssemblyReferences
  输出的 Target:_GetBuildOutputFilesWithTfm
  输出的 Target:_GetTfmSpecificContentForPackage
  输出的 Target:_GetDebugSymbolsWithTfm
  输出的 Target:_AddPriFileToPackBuildOutput
  输出的 Target:_GetPackageFiles

参考资料

如何在 csproj 中用 C# 代码写一个内联的编译任务 Task

dotnet 职业技术学院 发布于 2019-03-01

我之前写过一些改变 MSBuild 编译过程的一些博客,包括利用 Microsoft.NET.Sdk 中各种自带的 Task 来执行各种各样的编译任务。更复杂的任务难以直接利用自带的 Task 实现,需要自己写 Task。

本文介绍非常简单的 Task 的编写方式 —— 在 csproj 文件中写内联的 Task。


前置知识

在阅读本文之前,你至少需要懂得:

  • csproj 文件的结构以及编译过程
  • Target 是什么,Task 是什么

所以如果你不懂或者理不清,则请先阅读:

关于 Task 的理解,我有一些介绍自带 Task 的博客以及如何编写 Task 的教程:

编写内联的编译任务(Task)

如果你阅读了前面的博客,那么大致知道如何写一个在编译期间执行的 Task。不过,默认你需要编写一个额外的项目来写 Task,然后将这个项目生成 dll 供编译过程通过 UsingTask 来使用。然而如果 Task 足够简单,那么依然需要那么复杂的过程显然开发成本过高。

于是现在可以编写内联的 Task:

  1. 内联任务的支持需要用到 Microsoft.Build.Tasks.v4.0.dll;
  2. 我们用 <![CDATA[ ]]> 来内嵌 C# 代码;
  3. 除了用 UsingTask 编写内联的 Task 外,我们需要额外编写一个 Target 来验证我们的内联 Task 能正常工作。

下面是一个最简单的内联编译任务:

<Project Sdk="Microsoft.NET.Sdk">
    <UsingTask TaskName="WalterlvDemoTask" TaskFactory="CodeTaskFactory"
               AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
        <Task>
            <Code Type="Fragment" Language="cs">
                <![CDATA[
        Console.WriteLine("Hello Walterlv!");
                ]]>
            </Code>
        </Task>
    </UsingTask>
<Project>

为了能够测试,我把完整的 csproj 文件贴出来:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net472</TargetFramework>
    </PropertyGroup>

    <UsingTask TaskName="WalterlvDemoTask" TaskFactory="CodeTaskFactory"
               AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
        <Task>
            <Code Type="Fragment" Language="cs">
                <![CDATA[
        Console.WriteLine("Hello Walterlv!");
                ]]>
            </Code>
        </Task>
    </UsingTask>

    <Target Name="WalterlvDemoTarget" AfterTargets="Build">
        <WalterlvDemoTask />
    </Target>

</Project>

目前内联编译仅适用于 MSBuild,而 dotnet build 尚不支持。现在在项目目录输入命令进行编译,可以在输出窗口看到我们内联编译中的输出内容:

msbuild

输出内容

编写更复杂的内联编译任务

阅读我的另一篇博客了解如何编写一个更复杂的内联编译任务:

在 MSBuild 编译项目时阻止输出所有的警告信息

dotnet 职业技术学院 发布于 2019-02-27

大型旧项目可能存在大量的 Warning,在编译之后 Visual Studio 会给出大量的警告。Visual Studio 中可以直接点掉警告,然而如果是通过命令行 msbuild 编译的,那如何不要让警告输出呢?


在使用 msbuild 命令编译项目的时候,如果存在大量的警告,输出量会非常多。如果我们使用 msbuild 命令编译来定位项目的编译错误,那么这些警告将会导致我们准确查找错误的效率明显降低。

当然,这种问题的首选解决方案是 —— 真的修复掉这些警告!!!

那么可以用什么方式临时关闭 msbuild 命令编译时的警告呢?可以输入如下命令:

msbuild /p:WarningLevel=0

这样在调试编译问题的时候,因警告而造成的大量输出信息就会少很多。

不过需要注意的是,这种方式不会关闭所有的警告,实际上这关闭的是 csc 命令的警告(CS 开头)。关于 csc 命令的警告可以参见:-warn (C# Compiler Options) - Microsoft Docs。于是,如果项目中存在 msbuild 的警告(MSB 开头),此方法依然还会输出,只不过如果是为了调试编译问题,那么依然会方便很多,因为 MSB 开头的警告会少非常多。

关于警告等级:

  • 0 关闭所有的警告。
  • 1 仅显示严重警告。
  • 2 显示 1 级的警告以及某些不太严重的警告,例如有关隐藏类成员的警告。
  • 3 显示级别 2 警告以及某些不太严重的警告,例如关于始终评估为 true 或 false 的表达式的警告。
  • 4 默认值 显示所有 3 级警告和普通信息警告。

参考资料

电脑总是意外从睡眠状态唤醒,可以找出原因然后解决

dotnet 职业技术学院 发布于 2019-02-27

在昏暗的夜晚,一个人躺在房间的床上,静静的思考着什么。突然间电脑屏幕亮了!什么鬼!到底是谁唤醒了我的电脑!!!

本文将介绍如何寻找唤醒电脑的真凶。


调查是谁唤醒了电脑

使用命令查看上一次是谁唤醒了电脑。

powercfg -lastwake

last wake

从图中可知上一次唤醒我计算机的是 英特尔® 以太网连接 I219-V 82186。

查看还有谁可以唤醒电脑

使用命令查看所有可以唤醒电脑的设备。

powercfg -devicequery wake_armed

wake armed

发现能唤醒我电脑的设备是键盘鼠标以及刚刚的以太网。

wake timers

查看下一次计划的唤醒

使用命令可以查看下一次计划的唤醒。

powercfg -waketimers

当然这只能查到计划的唤醒,类似鼠标键盘还有以太网这种根据硬件状态触发的唤醒是看不到的。

修复意外的唤醒

由于我不知道到底是谁通过以太网唤醒了我的电脑,所以我直接关掉以太网的唤醒即可。

前往设备管理器,找到刚刚发现的硬件设备,查看属性。

设备管理器

然后我关闭了此设备唤醒电脑的设置。

关闭唤醒电脑


参考资料

将友盟(cnzz)站点的访问数据报告分享给其他人

dotnet 职业技术学院 发布于 2019-02-27

友盟(cnzz)可以帮助我们分析站点的访问数据。不过如果有更多的决策者,则需要更多人可以访问到友盟的数据。

本文介绍两种将自己站点的访问数据报告分享给其他人的方法。


首先,你必须有一个友盟产品的账号,如果还没有,去 注册吧!

使用查看密码分享

设置

进入 站点列表 页面。

点击网站右侧的“设置”。

点击设置

然后进入查看密码标签,开启查看密码服务,然后输入查看密码。

输入查看密码

这种方式的好处在于非常简单,你只需要告诉他人你的查看密码,其他人随时可以点开你网站的数据统计链接查看站点的访问数据。

Web 端查看

在 Web 端点开站点底部的访问数据即可进入数据报表页面。输入查看密码即可查看。

进入 Web 端的访问数据页面

在移动端查看

cnzz 移动端下载安装后首页有四个按钮。点击“查看密码”后输入站点 Id 或扫码,然后输入密码即可查看数据。

不过移动端的 cnzz 做得很烂,如果登录过自己的站点查看数据,那么使用密码查看必崩,而且至今未修复。

移动端

使用授权账号分享

本来使用密码查看是非常方便的,但是为了解决崩溃问题,还是需要使用授权账号来查看数据。

设置

进入 站点列表 页面。

点击网站右上角的“授权”。

点击授权

如果还没有授权给其他人,则可以点击“添加授权账号”。

添加授权账号

然后输入对方的友盟账号和邮箱,添加对方的权限。

设置邀请信息

在 Web 端查看

进入 站点列表 页面,可以在被授权站点看到授权查看的站点数据了。

查看被授权网站的访问数据

在移动端查看

cnzz 移动端下载安装后首页有四个按钮。点击“网站统计”后可以看到自己的站点和被授权的站点。

移动端

全局或为单独的项目添加自定义的 NuGet 源

dotnet 职业技术学院 发布于 2019-02-27

本文介绍如何添加自定义的 NuGet 源。包括全局所有项目生效的 NuGet 源和仅在某些特定项目中生效的 NuGet 源。


你可以前往 我收集的各种公有 NuGet 源 以发现更多的 NuGet 源,然后使用本文的方法添加到你自己的配置中。

使用命令行添加

在使用命令行之前,你需要先在 https://www.nuget.org/downloads 下载最新的 nuget.exe 然后加入到环境变量中。

现在,我们使用命令行来添加一个包含各种日构建版本的 NuGet 源 MyGet:

nuget sources add -Name "MyGet" -Source "https://dotnet.myget.org/F/dotnet-core/api/v3/index.json"

如果你添加的只是一个镜像源(比如华为云 huaweicloud),那么其功能和官方源是重合的,可以禁用掉官方源:

nuget sources Disable -Name "nuget.org"
nuget sources add -Name "huaweicloud" -Source "https://mirrors.huaweicloud.com/repository/nuget/v3/index.json"

在 Visual Studio 中添加

在 Visual Studio 中打开 工具 -> 选项 -> NuGet 包管理器 -> 包源:

管理包源

然后在界面上添加、删除、启用和禁用 NuGet 源。

值得注意的是:

  1. 在 Visual Studio 中是不能禁用掉官方源 nuget.org 的,无论你如何取消勾选,实际都不会生效。
    • 如果要取消,你需要用命令行或者手工编辑配置文件。
  2. 你可以添加一个本地路径作为本地 NuGet 源,而那个路径只要存在 *.nupkg 文件就够了。
    • 对于 .NET Core 项目,勾选编译后生成 NuGet 包则会在输出路径生成这样的文件,于是你可以本地调试。

直接修改配置文件

NuGet 的全局配置文件在 %AppData\NuGet\NuGet.config,例如:

C:\Users\lvyi\AppData\Roaming\NuGet\NuGet.Config

直接修改这个文件的效果跟使用命令行和 Visual Studio 的界面配置是等价的。

<configuration>
  <packageSources>
    <add key="huaweicloud" value="https://repo.huaweicloud.com/repository/nuget/v3/index.json" />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
    <add key="Walterlv.Debug" value="C:\Users\lvyi\Debug\Walterlv.NuGet" />
    <add key="Microsoft Visual Studio Offline Packages" value="C:\Program Files (x86)\Microsoft SDKs\NuGetPackages\" />
    <add key="MyGet" value="https://dotnet.myget.org/F/dotnet-core/api/v3/index.json" />
  </packageSources>
  <disabledPackageSources>
    <add key="Microsoft Visual Studio Offline Packages" value="true" />
    <add key="Walterlv.Debug" value="true" />
    <add key="nuget.org" value="true" />
  </disabledPackageSources>
</configuration>

为单独的项目添加自定义的 NuGet 源

NuGet.config 文件是有优先级的。nuget.exe 会先把全局配置加载进来;然后从当前目录中寻找 NuGet.config 文件,如果没找到就去上一级目录找,一直找到驱动器的根目录;找到后添加到已经加载好的全局配置中成为一个合并的配置。

所以我们只需要在项目的根目录放一个 NuGet.config 文件并填写相比于全局 NuGet.config 新增的配置即可为单独的项目添加 NuGet 配置。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <!-- 下一行的 clear 如果取消了注释,那么就会清除掉全局的 NuGet 源,而注释掉可以继承全局 NuGet 源,只是额外添加。 -->
    <!-- <clear /> -->
    <add key="MyGet" value="https://dotnet.myget.org/F/dotnet-core/api/v3/index.json" />
  </packageSources>
</configuration>

2019-2-24-2019-2-24-C#中的弃元

dotnet 职业技术学院 发布于 2019-02-24

从 C# 7.0 开始,C# 支持弃元,这是一种在应用程序代码中人为取消使用的临时虚拟变量。


使用过ConcurrentDictionary<T,T>的同学肯定经历过这样的痛苦

var dict = new ConcurrentDictionary<int,int>
dict[1]=1;
var result = dict.TryRemove(1, out var value);
Console.WriteLine(result);

我明明没有用到字典中删除的值,但是还是非要将这个值赋给某个变量。

有了弃元之后,你就可以写出这样的代码

var dict = new ConcurrentDictionary<int,int>();
dict[1]=1;
var result = dict.TryRemove(1,out _);
Console.WriteLine(result);

我们看到,不但没有变量赋值,连类型声明都不需要了,代替的只是使用了一个下划线_

当然弃元不只是书写和语义上的提升,它还可以减少内存分配。

除了out参数,我们还可以再元组析构,switch等语句中使用弃元写出优雅代码

例如,下面的例子我们只希望使用到日期中的年份

var (year,_,_) = GetDate();

private (string,string,string) GetDate()
{
	//...
    return (year,month,day);
}

例如,我们在switch的模式匹配中不期望对指定类型的对象进行使用

Foo("10");

void Foo(object item)
{
	switch (item)
	{
	     case int val:
             Console.WriteLine(val);
	         break;
	     case string _:
	         Console.WriteLine("Error");
	         break;
	}
}

参考链接:

2019-2-24-元组投影初始值设定项

dotnet 职业技术学院 发布于 2019-02-24

使用过命名元组的同学都知道,命名元组可以使用“有意义的字段名”来代替Item,用于表示元组的成员。在C#7.1中引入了“元组投影初始值设定项”(tuple projection initializers)提升了命名元组的编程体验


一般情况下,我们会采用下面这种方式进行命名元组的创建

var firstName = "Huang";
var secendName = "Tengxiao";
var fullName = (firstName:firstName,secendName:secendName);
Console.WriteLine(fullName.firstName);
Console.WriteLine(fullName.secendName);

但是在C#7.1之后可以使用如下写法,在式子中,元组采用构造时的变量名称对字段进行初始化

var firstName = "Huang";
var secendName = "Tengxiao";
var fullName = (firstName,secendName);
Console.WriteLine(fullName.firstName);
Console.WriteLine(fullName.secendName);

另外对于没有提供变量名称的初始化方式,元组会采用匿名元组默认的Item名称对字段名称进行初始化。例如

1550988997023

另外在以下两种情况下,不会将候选字段名称投影到元组字段:

  1. 候选名称是保留元组名称时。 示例包括 ItemX、ToString、 或 Rest。
  2. 候选名称重复了另一元组的显式或隐式字段名称时。

如下面的例子,在使用保留字,或者出现重名的时候,都会采用匿名元组默认的Item名称对字段名称进行初始化。

var Item3 = "Huang";
var Rest = "Tengxiao";
var fullName = (Item3,Rest);
Console.WriteLine(fullName.Item1);
Console.WriteLine(fullName.Item2);
var pt1 = (X: 3, Y: 0);
var pt2 = (X: 3, Y: 4);

var xCoords = (pt1.X, pt2.X);
Console.WriteLine(xCoords.Item1);
Console.WriteLine(xCoords.Item2);

不过有个小tips,对于c#这种区分大小写命名的语言,只要采用如下的小写命名就不会同保留字产生冲突。

(快去统一你们团队的命名元组编程规范吧~)

var item3 = "Huang";
var rest = "Tengxiao";
var fullName = (item3,rest);
Console.WriteLine(fullName.item3);
Console.WriteLine(fullName.rest);

参考链接:

2019-2-24-元组的相等性比较

dotnet 职业技术学院 发布于 2019-02-24

元组作为轻量级的数据结构,在c#中具有广泛的引用。但是元组的比较一直以来都是对于成员的依次比较。好在C#7.3开始,引入了元素的相等性比较,让元组的易用性有了大幅提升。


绑定

微软对此的介绍是“从 C# 7.3 开始,元组类型支持 == 和 != 运算符。 这些运算符按顺序将左边参数的每个成员与右边参数的每个成员进行比较,且比较是短路计算”

所以我们可以写出这样的代码,对元组进行比较

var left = (5, 10);
var right = (5, 10);
Console.WriteLine(left == right); 

此外,元组的比较也支持可空类型的提升转换,以及类型的隐式转换,例如下面代码中可空类型与非空类型的比较,(int,int)和(long,long)之间的比较

var left = (5, 10);
var right = (5, 10);

(int a, int b)? nullableTuple = right;
Console.WriteLine(left == nullableTuple.Value);
Console.WriteLine(left == nullableTuple);

(long a, long b) longTuple = (5, 10);
Console.WriteLine(left == longTuple);

参考链接:

使用 Visual Studio 调试多进程的程序

dotnet 职业技术学院 发布于 2019-02-21

当你的编写的是一个多进程的程序的时候,调试起来可能会比较困难,因为 Visual Studio 默认只会把你当前设置的启动项目的启动调试。

本文将介绍几种用 Visual Studio 调试多进程程序的方法,然后给出每种方法的适用条件和优劣。


Visual Studio 多启动项目(推荐)

在 Visual Studio 的解决方案上点击右键,属性。在公共属性节点中选择启动项目。

在这里,你可以给多个项目都设置成启动项目,就像下图这样:

设置多启动项目

当然,这些项目都必须要是能够启动的才行(不一定是可执行程序)。

此方案的好处是 Visual Studio 原生支持。但此方案的使用必须满足两个前提:

  1. 要调试的多个进程必须是不同的项目编译出来的;
  2. 这些项目之间的启动顺序不能有明显的依赖关系(所以你可能需要修改你的代码使得这两个进程之间可以互相唤起)。

Microsoft Child Process Debugging Power Tool 插件(推荐)

安装和配置插件

请先安装 Microsoft Child Process Debugging Power Tool 插件。

安装插件后启动 Visual Studio,可以在 Debug -> Other Debugging Targets 中找到 Child Process Debugging Settings。

打开 Child Process Debugging Settings

然后你可以按照下图的设置开启此项目的子进程调试:

设置子进程调试

配置项目启动选项

但是,子进程要能够调试,你还必须开启混合模式调试,开启方法请参见我的另一篇博客:在 Visual Studio 新旧不同的 csproj 项目格式中启用混合模式调试程序(开启本机代码调试) - walterlv。

现在,你只需要开始调试你的程序,那么你程序中启动的新的子进程都将可以自动加入调试。

例子源码和效果

现在,我们拿下面这段代码作为例子来尝试子进程的调试。下面的代码中,if 中的代码会运行在子进程中,而 else 中的代码会运行在主进程中。

using System;
using System.Diagnostics;
using System.Linq;

namespace Walterlv.Debugging
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Any())
            {
                Console.WriteLine("Walterlv child application");
                Console.WriteLine(string.Join(Environment.NewLine, args));
                Console.ReadLine();
            }
            else
            {
                Console.WriteLine("Walterlv main application");
                var process = new Process
                {
                    StartInfo = new ProcessStartInfo(Process.GetCurrentProcess().MainModule.FileName, "--child"),
                };
                process.Start();
                process.WaitForExit();
            }
        }
    }
}

我们在 if 和 else 中都打上断点。正常情况下运行,只有 else 中的代码可以进断点;而如果以上子进程调试配置正确,那么两边你都可以进入断点(如下图)。

子进程进入了调试断点

值得注意的是,只要启动了本机代码调试,就不能在程序暂停之后修改代码了(像平时调试纯托管代码那样)。

在代码中编写“附加调试器”

调用 Debugger.Launch() 可以启动一个调试器来调试此进程。于是我们可以在我们被调试的程序中写下如下代码:

#if DEBUG
    if (!Debugger.IsAttached)
    {
        Debugger.Launch();
    }
#endif

仅在 DEBUG 条件下,如果当前没有附加任何调试器,那么就启动一个新的调试器来调试它。

当存在以上代码时,运行会弹出一个对话框,用于选择调试器。

选择调试器

这里选择的调试器有个不太方便的地方,如果调试器已经在使用,那么就不能选择。对于我们目前的场景,我们的主进程已经在调试了,所以子进程选择调试器的时候不能再选择主进程调试所用的 Visual Studio 了,而只能选择一个新的 Visual Studio;这一点很不方便。

对于此方法,我的建议是平常不要在团队项目中使用(这会让团队中的其他人不方便)。但是由于代码简单不需要配置,所以临时使用的话还是非常建议的。

在代码中调用 Visual Studio 的 COM 组件 API

编写中……

总结

综上,虽然我给出了 4 种不同的方法,但实际上没有任何一种方法能够像我们调试单个原生托管程序那样方便。每一种方法都各有优劣,一般情况下建议你使用我标注了“推荐”的方法;不过也建议针对不同的情况采用不同的方案。

  1. 简单的个人项目,希望快速开始多进程/子进程调试
    • 使用附加调试器
  2. 你有多个项目组成的多进程,并且这些进程恰好可以互相唤起,它们之间的启动顺序不影响父子进程的组成
    • 使用 Visual Studio 的多启动项目
  3. 你只有单个项目组成的多进程,或者多个进程之间依赖于启动顺序来组成父子进程

参考资料

将 svn 仓库迁移到 git 仓库

dotnet 职业技术学院 发布于 2019-02-20

我找到了一个很久很久以前编写的项目,然而当时是使用 svn 进行版本管理的。然而现在的版本管理全部是 git,不愿意再装一个 svn 工具来管理这些古老的项目,于是打算将其迁移到 git 中。

本文介绍如何将古老的 svn 项目迁移到 git。


找回 svn 仓库的 url

如果你能记得你 svn 仓库的 url,或者这个仓库是一个纯本地仓库,那么你直接复制这个 url 就好了。

然而如果这是一个有 svn 远程服务器的仓库,那么你可能依然需要临时安装一下 svn 工具。我们只是为了拿回 url 而已。

这里我使用当时使用的小乌龟 TortoiseSVN。在 svn 仓库空白处右击选择版本库浏览器(Repo-browser),小乌龟会自动定位到当前仓库所在的远程 svn 服务器的对应文件夹。

版本库浏览器

我们所要做的只有一件事——复制顶部那个 url。

得到了这个 url 后,像我这种洁癖就卸载 TortoiseSVN 了。

将 svn 仓库迁移到 git 仓库

命令行

在一个新的文件夹中,我们输入如下命令:

git.exe svn clone "https://svn.walterlv.com/LvYi/Timer" ".\Walterlv.RepoFromSvn"

如果那个 svn 目录中包含 trunk、branches 和 tags 结构,那么可以在后面添加相应的参数以便在 clone 完成后保留分支和标签信息。

git.exe svn clone "https://svn.walterlv.com/LvYi/Timer" ".\Walterlv.RepoFromSvn" -T trunk -b branches -t tags

需要注意的是,上面的 Walterlv.RepoFromSvn 文件夹是不允许提前存在的,如果存在将无法迁移成功。

TortoiseGit

这里特地照顾一下从 TortoiseSVN 迁移来继续考虑 TortoiseGit 的小伙伴。在 TortoiseGit 中的操作是:

  1. 在某个文件夹中右键(或者 Shift+右键)
  2. 选择克隆
  3. 按照下图填写来自 url 的远程服务器 url 和本地文件夹,并打勾“从SVN版本库”

TortoiseGit 上的迁移 SVN 操作


参考资料

.NET/C# 将一个命令行参数字符串转换为命令行参数数组 args

dotnet 职业技术学院 发布于 2019-02-19

我们通常得到的命令行参数是一个字符串数组 string[] args,以至于很多的命令行解析库也是使用数组作为解析的参数来源。

然而如我我们得到了一整个命令行字符串呢?这个时候可能我们原有代码中用于解析命令行的库或者其他辅助函数不能用了。那么如何转换成数组呢?


在 Windows 系统中有函数 CommandLineToArgvW 可以直接将一个字符串转换为命令行参数数组,我们可以直接使用这个函数。

LPWSTR * CommandLineToArgvW(
  LPCWSTR lpCmdLine,
  int     *pNumArgs
);

此函数在 shell32.dll 中,于是我们可以在 C# 中调用此函数。

为了方便使用,我将其封装成了一个静态方法。

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

namespace Walterlv
{
    public static class CommandLineExtensions
    {
        public static string[] ConvertCommandLineToArgs(string commandLine)
        {
            var argv = CommandLineToArgvW(commandLine, out var argc);
            if (argv == IntPtr.Zero)
            {
                throw new Win32Exception("在转换命令行参数的时候出现了错误。");
            }

            try
            {
                var args = new string[argc];
                for (var i = 0; i < args.Length; i++)
                {
                    var p = Marshal.ReadIntPtr(argv, i * IntPtr.Size);
                    args[i] = Marshal.PtrToStringUni(p);
                }

                return args;
            }
            finally
            {
                Marshal.FreeHGlobal(argv);
            }
        }

        [DllImport("shell32.dll", SetLastError = true)]
        static extern IntPtr CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs);
    }
}

参考资料

git 合并策略

dotnet 职业技术学院 发布于 2019-02-15

不清楚 git 冲突的表示方法,不了解 git 的合并原理,不知道 git 解冲突的多种策略。即便如此,大多数人依然可以正常使用 git 完成合并、拉取操作,并且解一些冲突。这得益于 git 默认情况下的合并方式可以处理大多数情况下的正常合并。

然而,你是否遭遇 git 自动合并炸掉的情况?命名提示没有冲突,代码却早已无法编译通过。

本文将介绍 git 的合并策略,你可能可以更好的使用不同的策略来解决冲突。


git 合并策略

典型的使用指定 git 合并策略的命令这么写:

$ git merge 要合并进来的分支名 --strategy=合并策略

例如:

$ git merge origin/master --strategy=resolve

或者使用简写 -s,例如:

$ git merge origin/master -s resolve

可以指定的合并策略有:

  • resolve
  • recursive
  • octopus
  • ours
  • subtree

resolve

这使用的是三路合并算法。不过我们在 git 的合并原理(递归三路合并算法) 中说过,普通的三路合并算法会存在发现多个共同祖先的问题。此策略会“仔细地”寻找其中一个共同祖先。

由于不需要递归合并出虚拟节点,所以此方法合并时会比较快速,但也可能会带来更多冲突。不敢说带来更多冲突是好事还是坏事,因为自动合并成功并不一定意味着在代码含义上也算是正确的合并。所以如果自动合并总是成功但代码含义上会失败,可以考虑此合并策略,这将让更多的冲突变成手工合并而不是自动合并。

recursive

这是默认的合并策略,如果你不指定策略参数,那么将使用这个合并策略。这将直接使用递归三路合并算法进行合并,详见:git 的合并原理(递归三路合并算法)。

当指定为此策略时,可以额外指定下面的这些参数,方法是:

$ git merge 要合并进来的分支名 --strategy=合并策略 -X diff-algorithm=参数

例如:

$ git merge origin/master -s recursive -X diff-algorithm=patience

由于 recursive 是默认的合并策略,所以可以简化成:

$ git merge origin/master -X diff-algorithm=patience

ours

如果不冲突,那么与默认的合并方式相同。但如果发生冲突,将自动应用自己这一方的修改。

注意策略里面也有一个 ours,与这个不同的。

theirs

这与 ours 相反。如果不冲突,那么与默认的合并方式相同。但如果发生冲突,将自动应用来自其他人的修改(也就是 merge 参数中指定的那个分支的修改)。

patience

此策略的名称叫“耐心”,因为 git 将话费更多的时间来进行合并一些看起来不怎么重要的行,合并的结果也更加准确。当然,使用的算法是 recursive 即递归三路合并算法。

不过此名称也难以准确描述到底如何准确,不过可以举一个例子来说明:

int Foo()
{
    // 一些省略的代码。
}

int Baz()
{
    // 一些省略的代码。
}

然后在这两个函数中增加另一个函数:

int Bar()
{
    // 一些省略的代码。
}

默认情况下 git 会认为修改是这样的:

+ }
+
+ int Bar()
+ {
+     // 一些省略的代码。

然而使用 patience 策略后,git 将认为修改是这样的:

+ int Bar()
+ {
+     // 一些省略的代码。
+ }
+

如果你经常合并出现这些括号丢失或者符号不再匹配的问题,可以考虑使用 patience 策略进行合并。

no-renames

默认情况下 git 会识别出你重命名或者移动了文件,以便在你移动了文件之后依然可以与原文件进行合并。如果指定此策略,那么 git 将不再识别重命名,而是当作增加和删除了文件。

其他的参数

  • diff-algorithm=[patience|minimal|histogram|myers]
  • renormalize
  • no-renormalize
  • find-renames[=<n>]
  • rename-threshold=<n>
  • subtree[=<path>]

octopus

又是一个奇怪的名字——章鱼。章鱼有很多的触手,此合并策略就像这么多的触手一样。

此策略允许合并多个 git 提交节点(分支)。不过,如果会出现需要手工解决的冲突,那么此策略将不会执行。

此策略就是用来把多个分支聚集在一起的。

$ git merge t/lvyi t/walterlv -s octopus
error: Merge requires file-level merging
Trying really trivial in-index merge...
Nope.
Merge with strategy octopus failed.

ours

在合并的时候,无论有多少个合并分支,当前分支就直接是最终的合并结果。无论其他人有多少修改,在此次合并之后,都将不存在(当然历史里面还有)。

你可能觉得这种丢失改动的合并策略没有什么用。但如果你准备重新在你的仓库中进行开发(程序员最喜欢的重构),那么当你的修改与旧分支合并时,采用此合并策略就非常有用,你新的重构代码将完全不会被旧分支的改动所影响。

注意 recursive 策略中也有一个 ours 参数,与这个不同的。

subtree

此策略使用的是修改后的递归三路合并算法。与 recursive 不同的是,此策略会将合并的两个分支的其中一个视为另一个的子树,就像 git subtree 中使用的子树一样。


参考资料

git 的合并原理(递归三路合并算法)

dotnet 职业技术学院 发布于 2019-02-14

如果 git 只是一行行比较,然后把不同的行报成冲突,那么你在合并的时候可能会遇到大量的冲突;这显然不是一个好的版本管理工具。

本文介绍 git 合并分支的原理。


git 的冲突表示

例如我们有这样的三个提交 a、b、c。a、b 是在 master 上的其他修改,c 是我自己基于 master 上的 a 的修改。

现在,将 master 分支合并到我自己的 t/walterlv 分支:

git 提交树

a 提交:

Console.WriteLine("Hello World!");

b 提交:

Console.WriteLine("Hello Master!");

c 提交:

Console.WriteLine("Hello Walterlv!");

于是现在将 c 提交合并到 master 的时候就会出现冲突。冲突的表示会是这样:

<<<<<<< HEAD
Console.WriteLine("Hello Walterlv!");
=======
Console.WriteLine("Hello Master!");
>>>>>>> master

以 <<<<<<< 表示冲突开头,>>>>>>> 表示冲突结尾,======= 分隔冲突的不同修改。上面是 HEAD,也就是在合并之前的工作目录上的最近提交;下面是合并进来的分支,通常是来自其他人的修改。

三路合并

加入上面的 b 提交修改的是其他文件。然后依然按照前面的方式进行合并。

当出现冲突时,如果你只能看到不同的两行,那么你根本不知道究竟应该如何修改的。就像下面这样:

<<<<<<< HEAD
Console.WriteLine("Hello Walterlv!");
=======
Console.WriteLine("Hello World!");
>>>>>>> master

只看这点你怎么知道两行应该采用哪一行?这是二路合并算法带来的问题。在此算法下,你的每次拉取代码可能都会带来大量的冲突;这显然是不能接受的。

三路合并算法会找到合并的这两个提交的共同祖先。在这里也就是 a 提交。master 的此文件对 a 没有修改,而当前分支 t/walterlv 对此文件有修改,于是就会应用此分支的修改。

当然,前一节的问题依然会冲突,因为两个分支相对于共同的祖先节点 a 对同一个文件都有修改。

递归三路合并

从上面我们可以看到三路合并解决了二路合并中对于相同行不知道用哪一个的问题。不过实际的 git 提交树会更加复杂,就像下图那样纵横交错:

纵横交错的 git 提交树

相比于本文一开始,我们只是新增了两个提交而已,现在 f 提交是我们正在合并的提交。

如果现在找 e 和 d 的共同祖先,你会发现并不唯一,b 和 c 都是。那么此时怎么合并呢?

  1. git 会首先将 b 和 c 合并成一个虚拟的提交 x,这个 x 当作 e 和 d 的共同祖先。
  2. 而要合并 b 和 c,也需要进行同样的操作,即找到一个共同的祖先 a。

我们这里的 a、b、c 只是个比较简单的例子,实际上提交树往往更加复杂,这就需要不断重复以上操作以便找到一个真实存在的共同祖先,而这个操作是递归的。这便是“递归三路合并”的含义。

这是 git 合并时默认采用的策略。

快进式合并

git 还有非常简单的快进式(Fast-Forward)合并。快进式合并要求合并的两个分支(或提交)必须是祖孙/父子关系。例如上面的 e 和 d 并不满足此关系,所以无法进行快进式合并。

在上面的例子合并出了 f 之后,如果将 t/walterlv 合并到 master,那么就可以使用快进式合并。这时,直接将 master 分支的 HEAD 指向 f 提交即完成了合并。当然,可以生成也可以不生成新的 g 提交,但内容与 f 的内容完全一样。


参考资料

ReSharper 在 C 盘占用了太多空间了,本文告诉你如何安全地删除或转移这些文件

dotnet 职业技术学院 发布于 2019-01-30

一个不小心,我的 SSD 又满了。到底是谁占用了那么多的空间!如果你是 ReSharper 的重度用户,那么可能你的调查结果会直指 JetBrains ReSharper。

本文将告诉你如何安全地删除这些文件来释放你的 C 盘空间,然后在 ReSharper 中设置其他的缓存目录。


消失的 C 盘空间

SSD 很贵的,看看都满成什么样儿了……我一个 SSD 分成了 C 和 D 两个分区,都满了。

近乎满了的 SSD

你可以使用 SpaceSniffer 来快速调查占用你大量 C 盘空间的到底是些什么文件。我之前写过一篇文章介绍如何使用它:

当你是 ReSharper 的重度用户的时候,你很有可能会看到如下的场景:

JetBrains 家的软件竟然占据了这么多空间

是的,JetBrains 家的软件竟然占用了 17.2GB 的 C 盘空间!他们一定认为所有的用户都是土豪,能够买 500GB 以上的 SSD 全部分配给 C 盘。

好的,吐槽就到这里,我们进入正题——删除这些文件。

删除 ReSharper 的缓存目录

注意:只有 Transient 文件夹是可以删除的!

ReSharper 安装时的目录都在 %LocalAppData%\JetBrains 中。虽然运行时的缓存也在这里,但是如果你直接把这个目录删掉了,那么 ReSharper 插件以及 JetBrains 全家桶也就不能正常使用了。

Transient 意思跟 Temporary 差不多,就是短暂使用的文件。不过 ReSharper 竟然在这里堆了这么多。

Transient

删除掉这个文件夹不影响 ReSharper 及其他 JetBrains 全家桶的正常运行。

ReSharper 在设置中提供了清除缓存的按钮,但那个按钮点了其实释放不了多少空间的,本文最后一句将说明这个问题。

删除 Transient 目录

转移 ReSharper 的缓存目录

  1. 从 Visual Studio 的菜单中进入 ReSharper 的设置界面:ReSharper -> Options;
  2. 进入缓存设置选项:Environment -> General -> Caches -> Store solution。

在这里可以修改 ReSharper 缓存文件的存储位置。

不过可得提醒你一下,ReSharper 这么耗性能的插件,还是老老实实放 SSD 里面吧,SSD 再怎么贵比起你的时间来说可便宜多了呀!

ReSharper Options

更改缓存目录

可以在这个界面中看到,ReSharper 其实是提供了清除缓存的按钮(Clear)的,但是这个按钮点击之后其实只是会删除当前项目的缓存。而实际上 ReSharper 在你的电脑上积攒久了是众多缓存文件一起占用的太多空间,只删除最近正在使用的这个项目其实根本释放不了多少空间的。(比如我打开我的 Walterlv.CloudKeyboard 项目清除结果只删掉了不到 100M 的空间。)


参考资料