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 文件贴出来了。如果你希望在你的项目中去使用,可以只复制 UsingTaskTarget 两个部分。

<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:
  输出的 TargetOutputAll
  输出的 Target_CheckForUnsupportedTargetFramework
  输出的 Target_CollectTargetFrameworkForTelemetry
  输出的 Target_CheckForUnsupportedNETCoreVersion
  输出的 Target_CheckForUnsupportedNETStandardVersion
  输出的 Target_CheckForUnsupportedAppHostUsage
  输出的 Target_CheckForMismatchingPlatform
  输出的 Target_CheckForNETCoreSdkIsPreview
  输出的 TargetAdjustDefaultPlatformTargetForNetFrameworkExeWithNoNativeCopyLocalItems
  输出的 TargetCreateManifestResourceNames
  输出的 TargetResolveCodeAnalysisRuleSet
  输出的 TargetXamlPreCompile
  输出的 TargetShimReferencePathsWhenCommonTargetsDoesNotUnderstandReferenceAssemblies
  输出的 Target_BeforeVBCSCoreCompile
  输出的 TargetInitializeSourceRootMappedPaths
  输出的 Target_InitializeSourceRootMappedPathsFromSourceControl
  输出的 Target_SetPathMapFromSourceRoots
  输出的 TargetCoreCompile
  输出的 TargetResolvePackageDependenciesDesignTime
  输出的 TargetCollectSDKReferencesDesignTime
  输出的 TargetCollectResolvedSDKReferencesDesignTime
  输出的 TargetCollectPackageReferences
  输出的 Target_CheckCompileDesignTimePrerequisite
  输出的 TargetCollectAnalyzersDesignTime
  输出的 TargetCollectResolvedCompilationReferencesDesignTime
  输出的 TargetCollectUpToDateCheckInputDesignTime
  输出的 TargetCollectUpToDateCheckOutputDesignTime
  输出的 TargetCollectUpToDateCheckBuiltDesignTime
  输出的 TargetCompileDesignTime
  输出的 Target_FixVCLibs120References
  输出的 Target_AddVCLibs140UniversalCrtDebugReference
  输出的 TargetInitializeSourceControlInformation
  输出的 Target_CheckForInvalidConfigurationAndPlatform
  输出的 TargetBuild
  输出的 TargetBeforeBuild
  输出的 TargetAfterBuild
  输出的 TargetCoreBuild
  输出的 TargetRebuild
  输出的 TargetBeforeRebuild
  输出的 TargetAfterRebuild
  输出的 TargetBuildGenerateSources
  输出的 TargetBuildGenerateSourcesTraverse
  输出的 TargetBuildCompile
  输出的 TargetBuildCompileTraverse
  输出的 TargetBuildLink
  输出的 TargetBuildLinkTraverse
  输出的 TargetCopyRunEnvironmentFiles
  输出的 TargetRun
  输出的 TargetBuildOnlySettings
  输出的 TargetPrepareForBuild
  输出的 TargetGetFrameworkPaths
  输出的 TargetGetReferenceAssemblyPaths
  输出的 TargetGetTargetFrameworkMoniker
  输出的 TargetGetTargetFrameworkMonikerDisplayName
  输出的 TargetGetTargetFrameworkDirectories
  输出的 TargetAssignLinkMetadata
  输出的 TargetPreBuildEvent
  输出的 TargetUnmanagedUnregistration
  输出的 TargetGetTargetFrameworkVersion
  输出的 TargetResolveReferences
  输出的 TargetBeforeResolveReferences
  输出的 TargetAfterResolveReferences
  输出的 TargetAssignProjectConfiguration
  输出的 Target_SplitProjectReferencesByFileExistence
  输出的 Target_GetProjectReferenceTargetFrameworkProperties
  输出的 TargetGetTargetFrameworks
  输出的 TargetGetTargetFrameworkProperties
  输出的 TargetPrepareProjectReferences
  输出的 TargetResolveProjectReferences
  输出的 TargetResolveProjectReferencesDesignTime
  输出的 TargetExpandSDKReferencesDesignTime
  输出的 TargetGetTargetPath
  输出的 TargetGetTargetPathWithTargetPlatformMoniker
  输出的 TargetGetNativeManifest
  输出的 TargetResolveNativeReferences
  输出的 TargetResolveAssemblyReferences
  输出的 TargetFindReferenceAssembliesForReferences
  输出的 TargetGenerateBindingRedirects
  输出的 TargetGenerateBindingRedirectsUpdateAppConfig
  输出的 TargetGetInstalledSDKLocations
  输出的 TargetResolveSDKReferences
  输出的 TargetResolveSDKReferencesDesignTime
  输出的 TargetFindInvalidProjectReferences
  输出的 TargetGetReferenceTargetPlatformMonikers
  输出的 TargetExpandSDKReferences
  输出的 TargetExportWindowsMDFile
  输出的 TargetResolveAssemblyReferencesDesignTime
  输出的 TargetDesignTimeResolveAssemblyReferences
  输出的 TargetResolveComReferences
  输出的 TargetResolveComReferencesDesignTime
  输出的 TargetPrepareResources
  输出的 TargetPrepareResourceNames
  输出的 TargetAssignTargetPaths
  输出的 TargetGetItemTargetPaths
  输出的 TargetSplitResourcesByCulture
  输出的 TargetCreateCustomManifestResourceNames
  输出的 TargetResGen
  输出的 TargetBeforeResGen
  输出的 TargetAfterResGen
  输出的 TargetCoreResGen
  输出的 TargetCompileLicxFiles
  输出的 TargetResolveKeySource
  输出的 TargetCompile
  输出的 Target_GenerateCompileInputs
  输出的 TargetGenerateTargetFrameworkMonikerAttribute
  输出的 TargetGenerateAdditionalSources
  输出的 TargetBeforeCompile
  输出的 TargetAfterCompile
  输出的 Target_TimeStampBeforeCompile
  输出的 Target_GenerateCompileDependencyCache
  输出的 Target_TimeStampAfterCompile
  输出的 Target_ComputeNonExistentFileProperty
  输出的 TargetGenerateSerializationAssemblies
  输出的 TargetCreateSatelliteAssemblies
  输出的 Target_GenerateSatelliteAssemblyInputs
  输出的 TargetGenerateSatelliteAssemblies
  输出的 TargetComputeIntermediateSatelliteAssemblies
  输出的 TargetSetWin32ManifestProperties
  输出的 Target_SetExternalWin32ManifestProperties
  输出的 Target_SetEmbeddedWin32ManifestProperties
  输出的 Target_GenerateResolvedDeploymentManifestEntryPoint
  输出的 TargetGenerateManifests
  输出的 TargetGenerateApplicationManifest
  输出的 Target_DeploymentComputeNativeManifestInfo
  输出的 Target_DeploymentComputeClickOnceManifestInfo
  输出的 Target_DeploymentGenerateTrustInfo
  输出的 TargetGenerateDeploymentManifest
  输出的 TargetPrepareForRun
  输出的 TargetCopyFilesToOutputDirectory
  输出的 Target_CopyFilesMarkedCopyLocal
  输出的 Target_CopySourceItemsToOutputDirectory
  输出的 TargetGetCopyToOutputDirectoryItems
  输出的 TargetGetCopyToPublishDirectoryItems
  输出的 Target_CopyOutOfDateSourceItemsToOutputDirectory
  输出的 Target_CopyOutOfDateSourceItemsToOutputDirectoryAlways
  输出的 Target_CopyAppConfigFile
  输出的 Target_CopyManifestFiles
  输出的 Target_CheckForCompileOutputs
  输出的 Target_SGenCheckForOutputs
  输出的 TargetUnmanagedRegistration
  输出的 TargetIncrementalClean
  输出的 Target_CleanGetCurrentAndPriorFileWrites
  输出的 TargetClean
  输出的 TargetBeforeClean
  输出的 TargetAfterClean
  输出的 TargetCleanReferencedProjects
  输出的 TargetCoreClean
  输出的 Target_CleanRecordFileWrites
  输出的 TargetCleanPublishFolder
  输出的 TargetPostBuildEvent
  输出的 TargetPublish
  输出的 Target_DeploymentUnpublishable
  输出的 TargetSetGenerateManifests
  输出的 TargetPublishOnly
  输出的 TargetBeforePublish
  输出的 TargetAfterPublish
  输出的 TargetPublishBuild
  输出的 Target_CopyFilesToPublishFolder
  输出的 Target_DeploymentGenerateBootstrapper
  输出的 Target_DeploymentSignClickOnceDeployment
  输出的 TargetAllProjectOutputGroups
  输出的 TargetBuiltProjectOutputGroup
  输出的 TargetDebugSymbolsProjectOutputGroup
  输出的 TargetDocumentationProjectOutputGroup
  输出的 TargetSatelliteDllsProjectOutputGroup
  输出的 TargetSourceFilesProjectOutputGroup
  输出的 TargetGetCompile
  输出的 TargetContentFilesProjectOutputGroup
  输出的 TargetSGenFilesOutputGroup
  输出的 TargetGetResolvedSDKReferences
  输出的 TargetCollectReferencedNuGetPackages
  输出的 TargetPriFilesOutputGroup
  输出的 TargetSDKRedistOutputGroup
  输出的 TargetAllProjectOutputGroupsDependencies
  输出的 TargetBuiltProjectOutputGroupDependencies
  输出的 TargetDebugSymbolsProjectOutputGroupDependencies
  输出的 TargetSatelliteDllsProjectOutputGroupDependencies
  输出的 TargetDocumentationProjectOutputGroupDependencies
  输出的 TargetSGenFilesOutputGroupDependencies
  输出的 TargetReferenceCopyLocalPathsOutputGroup
  输出的 TargetSetCABuildNativeEnvironmentVariables
  输出的 TargetRunCodeAnalysis
  输出的 TargetRunNativeCodeAnalysis
  输出的 TargetRunSelectedFileNativeCodeAnalysis
  输出的 TargetRunMergeNativeCodeAnalysis
  输出的 TargetImplicitlyExpandDesignTimeFacades
  输出的 TargetGetWinFXPath
  输出的 TargetDesignTimeMarkupCompilation
  输出的 TargetPrepareResourcesForSatelliteAssemblies
  输出的 Target_AfterCompileWinFXInternal
  输出的 TargetAfterCompileWinFX
  输出的 TargetAfterMarkupCompilePass1
  输出的 TargetAfterMarkupCompilePass2
  输出的 TargetMarkupCompilePass1
  输出的 TargetMarkupCompilePass2
  输出的 Target_CompileTemporaryAssembly
  输出的 TargetMarkupCompilePass2ForMainAssembly
  输出的 TargetGenerateTemporaryTargetAssembly
  输出的 TargetCleanupTemporaryTargetAssembly
  输出的 TargetAddIntermediateAssemblyToReferenceList
  输出的 TargetSatelliteOnlyMarkupCompilePass2
  输出的 TargetHostInBrowserValidation
  输出的 TargetSplashScreenValidation
  输出的 TargetResignApplicationManifest
  输出的 TargetSignDeploymentManifest
  输出的 TargetFileClassification
  输出的 TargetMainResourcesGeneration
  输出的 TargetSatelliteResourceGeneration
  输出的 TargetGenerateResourceWithCultureItem
  输出的 TargetCheckUid
  输出的 TargetUpdateUid
  输出的 TargetRemoveUid
  输出的 TargetMergeLocalizationDirectives
  输出的 TargetAssignWinFXEmbeddedResource
  输出的 TargetEntityDeploy
  输出的 TargetEntityDeploySplit
  输出的 TargetEntityDeployNonEmbeddedResources
  输出的 TargetEntityDeployEmbeddedResources
  输出的 TargetEntityClean
  输出的 TargetEntityDeploySetLogicalNames
  输出的 TargetDesignTimeXamlMarkupCompilation
  输出的 TargetInProcessXamlMarkupCompilePass1
  输出的 TargetCleanInProcessXamlGeneratedFiles
  输出的 TargetXamlMarkupCompileReadGeneratedFileList
  输出的 TargetXamlMarkupCompilePass1
  输出的 TargetXamlMarkupCompileAddFilesGenerated
  输出的 TargetXamlMarkupCompileReadPass2Flag
  输出的 TargetXamlTemporaryAssemblyGeneration
  输出的 TargetCompileTemporaryAssembly
  输出的 TargetXamlMarkupCompilePass2
  输出的 TargetXamlMarkupCompileAddExtensionFilesGenerated
  输出的 TargetGetCopyToOutputDirectoryXamlAppDefs
  输出的 TargetExpressionBuildExtension
  输出的 TargetValidationExtension
  输出的 TargetGenerateCompiledExpressionsTempFile
  输出的 TargetAddDeferredValidationErrorsFileToFileWrites
  输出的 TargetReportValidationBuildExtensionErrors
  输出的 TargetDeferredValidation
  输出的 TargetResolveTestReferences
  输出的 TargetCleanAppxPackage
  输出的 TargetGetPackagingOutputs
  输出的 TargetRestore
  输出的 TargetGenerateRestoreGraphFile
  输出的 Target_LoadRestoreGraphEntryPoints
  输出的 Target_FilterRestoreGraphProjectInputItems
  输出的 Target_GenerateRestoreGraph
  输出的 Target_GenerateRestoreGraphProjectEntry
  输出的 Target_GenerateRestoreSpecs
  输出的 Target_GenerateDotnetCliToolReferenceSpecs
  输出的 Target_GetProjectJsonPath
  输出的 Target_GetRestoreProjectStyle
  输出的 TargetEnableIntermediateOutputPathMismatchWarning
  输出的 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
  输出的 TargetDesktopBridgeCopyLocalOutputGroup
  输出的 TargetDesktopBridgeComFilesOutputGroup
  输出的 TargetGetDeployableContentReferenceOutputs
  输出的 TargetDockerResolveAppType
  输出的 TargetDockerUpdateComposeVsGeneratedFiles
  输出的 TargetDockerResolveTargetFramework
  输出的 TargetDockerComposeBuild
  输出的 TargetDockerPackageService
  输出的 TargetImplicitlyExpandNETStandardFacades
  输出的 Target_RemoveZipFileSuggestedRedirect
  输出的 TargetSetARM64AppxPackageInputsForInboxNetNative
  输出的 Target_CleanMdbFiles
  输出的 TargetPreXsdCodeGen
  输出的 TargetXsdCodeGen
  输出的 TargetXsdResolveReferencePath
  输出的 TargetCleanXsdCodeGen
  输出的 Target_SetTargetFrameworkMonikerAttribute
  输出的 TargetResolvePackageDependenciesForBuild
  输出的 TargetRunResolvePackageDependencies
  输出的 TargetResolvePackageAssets
  输出的 TargetFilterSatelliteResources
  输出的 TargetRunProduceContentAssets
  输出的 TargetReportAssetsLogMessages
  输出的 TargetResolveLockFileReferences
  输出的 TargetIncludeTransitiveProjectReferences
  输出的 TargetResolveLockFileAnalyzers
  输出的 Target_ComputeLockFileCopyLocal
  输出的 TargetResolveLockFileCopyLocalProjectDeps
  输出的 TargetCheckForImplicitPackageReferenceOverrides
  输出的 TargetCheckForDuplicateItems
  输出的 TargetGenerateBuildDependencyFile
  输出的 TargetGenerateBuildRuntimeConfigurationFiles
  输出的 TargetAddRuntimeConfigFileToBuiltProjectOutputGroupOutput
  输出的 Target_SdkBeforeClean
  输出的 Target_SdkBeforeRebuild
  输出的 Target_ComputeNETCoreBuildOutputFiles
  输出的 Target_ComputeReferenceAssemblies
  输出的 TargetCoreGenerateSatelliteAssemblies
  输出的 Target_GetAssemblyInfoFromTemplateFile
  输出的 Target_DefaultMicrosoftNETPlatformLibrary
  输出的 TargetGetAllRuntimeIdentifiers
  输出的 TargetGenerateAssemblyInfo
  输出的 TargetAddSourceRevisionToInformationalVersion
  输出的 TargetGetAssemblyAttributes
  输出的 TargetCreateGeneratedAssemblyInfoInputsCacheFile
  输出的 TargetCoreGenerateAssemblyInfo
  输出的 TargetGetAssemblyVersion
  输出的 TargetComposeStore
  输出的 TargetStoreWorkerMain
  输出的 TargetStoreWorkerMapper
  输出的 TargetStoreResolver
  输出的 TargetStoreWorkerPerformWork
  输出的 TargetStoreFinalizer
  输出的 Target_CopyResolvedOptimizedFiles
  输出的 TargetPrepareForComposeStore
  输出的 TargetPrepforRestoreForComposeStore
  输出的 TargetRestoreForComposeStore
  输出的 TargetComputeAndCopyFilesToStoreDirectory
  输出的 TargetCopyFilesToStoreDirectory
  输出的 Target_CopyResolvedUnOptimizedFiles
  输出的 Target_ComputeResolvedFilesToStoreTypes
  输出的 Target_SplitResolvedFiles
  输出的 Target_GetResolvedFilesToStore
  输出的 TargetComputeFilesToStore
  输出的 TargetPrepRestoreForStoreProjects
  输出的 TargetPrepOptimizer
  输出的 Target_RunOptimizer
  输出的 TargetRunCrossGen
  输出的 Target_InitializeBasicProps
  输出的 Target_GetCrossgenProps
  输出的 Target_SetupStageForCrossgen
  输出的 Target_RestoreCrossgen
  输出的 Target_CheckForObsoleteDotNetCliToolReferences
  输出的 Target_PublishBuildAlternative
  输出的 Target_PublishNoBuildAlternative
  输出的 Target_PreventProjectReferencesFromBuilding
  输出的 TargetPrepareForPublish
  输出的 TargetComputeAndCopyFilesToPublishDirectory
  输出的 TargetCopyFilesToPublishDirectory
  输出的 Target_CopyResolvedFilesToPublishPreserveNewest
  输出的 Target_CopyResolvedFilesToPublishAlways
  输出的 Target_ComputeResolvedFilesToPublishTypes
  输出的 TargetComputeFilesToPublish
  输出的 Target_ComputeNetPublishAssets
  输出的 TargetRunResolvePublishAssemblies
  输出的 TargetFilterPublishSatelliteResources
  输出的 Target_ComputeCopyToPublishDirectoryItems
  输出的 TargetDefaultCopyToPublishDirectoryMetadata
  输出的 TargetGeneratePublishDependencyFile
  输出的 Target_ComputeExcludeFromPublishPackageReferences
  输出的 Target_ParseTargetManifestFiles
  输出的 TargetGeneratePublishRuntimeConfigurationFile
  输出的 TargetDeployAppHost
  输出的 TargetPackTool
  输出的 TargetGenerateToolsSettingsFileFromBuildProperty
  输出的 TargetResolveApphostAsset
  输出的 TargetComputeDependencyFileCompilerOptions
  输出的 TargetComputeRefAssembliesToPublish
  输出的 Target_CopyReferenceOnlyAssembliesForBuild
  输出的 Target_HandlePackageFileConflicts
  输出的 Target_HandlePublishFileConflicts
  输出的 Target_GetOutputItemsFromPack
  输出的 Target_GetTargetFrameworksOutput
  输出的 Target_PackAsBuildAfterTarget
  输出的 Target_CleanPackageFiles
  输出的 Target_CalculateInputsOutputsForPack
  输出的 TargetPack
  输出的 Target_IntermediatePack
  输出的 TargetGenerateNuspec
  输出的 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 警告以及某些不太严重的警告,例如关于始终评估为 truefalse 的表达式的警告。
  • 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. 候选名称是保留元组名称时。 示例包括 ItemXToString、 或 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();
            }
        }
    }
}

我们在 ifelse 中都打上断点。正常情况下运行,只有 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 目录中包含 trunkbranchestags 结构,那么可以在后面添加相应的参数以便在 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 的空间。)


参考资料