Настройка сборки

Проекты MSBuild, использующие стандартный процесс сборки (импорт Microsoft.Common.props и Microsoft.Common.targets), имеют несколько обработчиков расширяемости, позволяющих настроить процесс сборки.

Многие настраиваемые операции сборки управляются свойствами. Важно знать, как и где задать значение свойства, чтобы получить нужный эффект. Свойства можно задать в командной строке (и в файлах ответов), в специальных файлах, таких как Directory.Build.props, в импортированных файлах или в файле проекта. Важно знать, где используется, задается или изменяется свойство, а также порядок импортированных файлов, включая неявный импорт из пакетов SDK, таких как пакет SDK для .NET.

Список свойств см. в разделе Общие свойства MSBuild.

Добавление аргументов в вызовы командной строки MSBuild для проекта

Глобальные свойства можно задать в командной строке. Глобальные свойства влияют на все сборки проекта, включая зависимости. Помните, что сборка проекта автоматически активирует возможную сборку для всех зависимостей проекта. Обычно MSBuild создает все зависимые проекты, которые устарели. Эти зависимые сборки проекта запускаются с теми же глобальными параметрами свойств из командной строки, что и исходный проект.

Файл Directory.Build.rsp, расположенный в исходном каталоге или на более высоком уровне, применяется к сборкам из командной строки проекта. Дополнительные сведения см. в разделе Файлы ответов MSBuild.

Directory.Build.props и Directory.Build.targets

Вы можете добавить новое свойство в любой проект, определив свойство в единственном файле Directory.Build.props в корневой папке с исходным кодом.

При запуске MSBuild Microsoft.Common.props выполняет поиск в структуре каталогов файла Directory.Build.props . Если MSBuild находит файл, то импортирует его и считывает свойства, определенные в этом файле. Пользовательский файл Directory.Build.props содержит настройки персонализации для проектов в своем каталоге.

Аналогичным образом Microsoft.Common.targets ищет Directory.Build.targets.

Directory.Build.props импортируется в начале последовательности импортированных файлов, что может быть важно, если требуется задать свойство, используемое импортом, особенно те, которые неявно импортируются с помощью атрибута Sdk , например при использовании пакета SDK для .NET в большинстве файлов проекта .NET.

Примечание

В файловых системах на основе Linux учитывается регистр. Убедитесь, что регистр имени файла Directory.Build.props полностью совпадает, иначе он не будет обнаружен во время сборки.

Дополнительные сведения см. в этой статье об ошибке на GitHub .

Пример Directory.Build.props

Например, если вы хотите предоставить всем проектам новую функцию Roslyn /deterministic через свойство $(Deterministic) в целевом объекте Roslyn CoreCompile, можно сделать следующее.

  1. Создайте в корне репозитория файл с именем Directory.Build.props.

  2. Добавьте в этот файл приведенный ниже XML-код.

    <Project>
     <PropertyGroup>
       <Deterministic>true</Deterministic>
     </PropertyGroup>
    </Project>
    
  3. Запустите MSBuild. Уже существующие в проекте файлы импорта Microsoft.Common.props и Microsoft.Common.targets находят новый файл и импортируют его.

Область поиска

При поиске файла Directory.Build.props MSBuild обходит структуру каталогов вверх от расположения проекта ($(MSBuildProjectFullPath)). Когда файл найден, поиск останавливается. Например, если $(MSBuildProjectFullPath) имеет значение c:\users\username\code\test\case1, MSBuild начнет поиск там, а затем будет переходить вверх по структуре каталогов, пока не найдет файл Directory.Build.props. Пример такого поиска по структуре каталогов представлен ниже.

c:\users\username\code\test\case1
c:\users\username\code\test
c:\users\username\code
c:\users\username
c:\users
c:\

Расположение файла решения не имеет значения для Directory.Build.props.

Порядок импорта

Directory.Build.props импортируется в Microsoft.Common.props очень рано, поэтому определенные позднее свойства ему недоступны. Так что не ссылайтесь на свойства, которые еще не определены (и оцениваются как пустые).

Свойства, которые задаются в файле Directory.Build.props, можно переопределить в любом другом месте файла проекта или в импортируемых файлах. Таким образом, устанавливаемые в файле Directory.Build.props свойства следует рассматривать как настройки по умолчанию для проектов.

Directory.Build.targets импортируется из Microsoft.Common.targets после импорта файлов .targets из пакетов NuGet. Таким образом, в нем могут быть переопределены свойства и целевые объекты, определенные в большей части логики сборки, а также заданы свойства для всех проектов, которые будут иметь приоритет над настройками отдельных проектов.

Если вам требуется задать свойство или определить целевой объект для отдельного проекта, которые будут переопределять любые ранее произведенные настройки, соответствующую логику следует помещать в файл проекта после окончательного импорта. Чтобы сделать это в проекте в стиле пакета SDK, сначала необходимо заменить атрибут стиля пакета SDK эквивалентными импортируемыми компонентами. См. статью Использование пакетов SDK проекта MSBuild.

Примечание

Обработчик MSBuild считывает все импортируемые файлы в процессе вычисления до начала выполнения сборки (включая любые PreBuildEvent) для проекта. Таким образом, эти файлы не должны быть изменены в рамках PreBuildEvent или любой другой части процесса сборки. Любые изменения вступают в силу только после следующего вызова MSBuild.exe или выполнения следующей сборки Visual Studio. Кроме того, если процесс сборки содержит много сборок проекта (как в случае с настройкой для различных версий или сборкой зависимых проектов), импортированные файлы, включая Directory.build.props, считываются при вычислении для каждой отдельной сборки проекта.

Вариант использования: многоуровневое слияние

Предположим, что вы используете следующую стандартную структуру решения:

\
  MySolution.sln
  Directory.Build.props     (1)
  \src
    Directory.Build.props   (2-src)
    \Project1
    \Project2
  \test
    Directory.Build.props   (2-test)
    \Project1Tests
    \Project2Tests

Допустим, вам нужны некоторые общие свойства для всех проектов (1) , а также общие свойства для проектов исходного кода(2-src) и общие свойства для проектов тестирования(2-test) .

Чтобы система MSBuild правильно объединила "внутренние" файлы (2-src и 2-test) с "внешним" файлом (1), обязательно учитывайте, что MSBuild прекращает сканирование при обнаружении файла Directory.Build.props. Чтобы продолжить сканирование и включить в слияние внешний файл, добавьте в оба внутренних файла следующее:

<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

Ниже кратко описан основной алгоритм работы MSBuild.

  • Для каждого проекта MSBuild находит ближайший файл Directory.Build.props, расположенный выше в структуре решения, объединяет его с параметрами по умолчанию и на этом останавливает сканирование.
  • Если вы хотите обнаружить и объединить несколько уровней, импортируйте "внешний" файл из "внутреннего", как показано выше (<Import...>).
  • Если "внешний" файл сам не импортирует ничего, расположенного выше, то сканирование останавливается на этом этапе.

Проще говоря, MSBuild останавливает поиск на первом файле Directory.Build.props, который ничего не импортирует.

Для более явного управления процессом импорта используйте свойства $(DirectoryBuildPropsPath), $(ImportDirectoryBuildProps), $(DirectoryBuildTargetsPath)и $(ImportDirectoryBuildTargets). Свойство $(DirectoryBuildPropsPath) указывает путь к используемому Directory.Build.props файлу; аналогично $(DirectoryBuildTargetsPath) указывает путь к файлу Directory.Build.targets .

Логические свойства $(ImportDirectoryBuildProps) и $(ImportDirectoryBuildTargets) по умолчанию имеют значение true , поэтому MSBuild обычно выполняет поиск этих файлов, но вы можете задать для них значение , false чтобы предотвратить их импорт MSBuild.

Выбор между добавлением свойств в PROPS-файл и TARGETS-файл

Поведение MSBuild зависит от порядка импорта, то есть всегда используется последнее обработанное определение свойства (или UsingTask, или целевого объекта).

При использовании явных инструкций import вы можете указывать их в любой точке файла .props или .targets. Наиболее распространено следующее соглашение:

  • файлы .props импортируются на ранних этапах импорта;

  • файлы .targets импортируются на поздних этапах импорта.

Такое соглашение поддерживается инструкциями import <Project Sdk="SdkName"> (например, файл Sdk.props импортируется первым, до всего содержимого файла, а файл Sdk.targets — последним, после всего содержимого файла).

При выборе места для размещения свойств используйте следующие рекомендации общего характера.

  • Для многих свойств нет никакого значения, где они определены,так как они не перезаписываются и считываются только во время выполнения.

  • Для поведения, которое можно настраивать для отдельного проекта, задавайте значения по умолчанию в файлах .props.

  • Старайтесь не устанавливать зависимые свойства в файлах .props путем считывания значений, которые могут быть изменены, поскольку такая настройка не будет применяться до того, как MSBuild считает пользовательский проект.

  • Устанавливайте зависимые свойства в файлах .targets, где уже будут учтены настройки из отдельных проектов.

  • Если вам нужно переопределить свойства, выполняйте это в файле .targets, после применения всех пользовательских настроек для отдельных проектов. Будьте внимательны при использовании производных свойств, поскольку их также нужно переопределять.

  • Включайте элементы в файлах .props (по условию значения свойства). Все свойства учитываются раньше, чем любые элементы, поэтому пользовательские настройки свойств для отдельных проектов будут учтены вовремя и позволят пользовательским проектам применить Remove или Update к любым элементам, полученным через операцию импорта.

  • Определяйте целевые объекты в файлах .targets. Тем не менее, если файл .targets импортируется из пакета SDK, на забывайте, что такой сценарий усложняет переопределение целевых объектов, ведь пользовательский проект по умолчанию не получает возможности переопределить их.

  • По возможности старайтесь настраивать свойства во время вычисления, а не изменять их внутри целевого объекта. Эта рекомендация упрощает загрузку проекта и понимание его механизма работы.

MSBuildProjectExtensionsPath

По умолчанию Microsoft.Common.props импортирует $(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).*.props, а Microsoft.Common.targets импортирует $(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).*.targets. По умолчанию MSBuildProjectExtensionsPath имеет значение $(BaseIntermediateOutputPath), obj/. NuGet использует этот механизм для ссылки на логику сборки, предоставляемую вместе с пакетами. Например, во время восстановления он создает файлы {project}.nuget.g.props, ссылающиеся на содержимое пакета.

Этот механизм расширяемости можно отключить, установив для свойства ImportProjectExtensionProps значение false в Directory.Build.props или до импорта Microsoft.Common.props.

Примечание

Отключение импортов MSBuildProjectExtensionsPath препятствует применению логики сборки, предоставляемой в пакетах NuGet, к проекту. Некоторым пакетам NuGet нужна работающая логика сборки, и если эта возможность отключена, они будут бесполезны.

Файл USER

Microsoft.Common.CurrentVersion.targets импортирует $(MSBuildProjectFullPath).user, если он существует, поэтому можно создать файл рядом с проектом с этим дополнительным расширением. Для долгосрочных изменений, которые вы собираетесь возвратить в систему управления версиями, рекомендуется изменить сам проект, чтобы тем, кто впоследствии будет обслуживать программу, не потребовалось разбираться в этом механизме расширения.

MSBuildExtensionsPath и MSBuildUserExtensionsPath

Предупреждение

Использование этих механизмов расширения затрудняет получение повторяемых сборок на разных компьютерах. Попробуйте использовать конфигурацию, которую можно возвратить в систему управления версиями и предоставить для общего доступа всем разработчикам базы кода.

По соглашению многие файлы базовой логики сборки импортируют

$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\{TargetFileName}\ImportBefore\*.targets

перед своим содержимым и

$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\{TargetFileName}\ImportAfter\*.targets

после него. Это соглашение позволяет установленным пакетам SDK дополнять логику сборки для распространенных типов проектов.

Поиск по той же структуре каталогов выполняется в $(MSBuildUserExtensionsPath), который является папкой конкретного пользователя %LOCALAPPDATA%\Microsoft\MSBuild. Файлы, помещенные в эту папку, импортируются для всех сборок соответствующего типа проекта, выполняемого с использованием учетных данных этого пользователя. Пользовательские расширения можно отключить, задав свойства, имя которых соответствует импортируемому файлу по шаблону ImportUserLocationsByWildcardBefore{ImportingFileNameWithNoDots}. Например, установив для ImportUserLocationsByWildcardBeforeMicrosoftCommonProps значение false, можно препятствовать импорту $(MSBuildUserExtensionsPath)\$(MSBuildToolsVersion)\Imports\Microsoft.Common.props\ImportBefore\*.

Настраиваемая конфигурация на основе языка проекта

Если вам нужно различное поведение в зависимости от языка .NET (C#, Visual Basic или F#), можно добавить в $(MSBuildProjectExtension) группы свойств с условиями, зависящими от расширения файла проекта, чтобы определить свойства и значения для конкретного языка.

<PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.vbproj'">
   <!-- Put VB-only property definitions here -->
</PropertyGroup>
<PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.fsproj'">
   <!-- Put F#-only property definitions here -->
</PropertyGroup>
<PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
   <!-- Put C#-only property definitions here -->
</PropertyGroup>

Обработка созданных файлов

В любой сборке файлы, созданные во время сборки, ведут себя не так, как статические файлы (например, исходные файлы). По этой причине важно понимать, как MSBuild создает проекты. Эти два этапа называются этапом оценки и этапом выполнения. На этапе оценки MSBuild считывает проект, импортирует все, создает свойства, расширяет маски элементов и настраивает процесс сборки. На этапе выполнения MSBuild выполняет сборку, выполняя целевые объекты и задачи с данными, проанализированными на этапе оценки.

Файлы, созданные во время выполнения, не существуют на этапе оценки, поэтому они не включаются в процесс сборки. Чтобы решить эту проблему, необходимо вручную добавить созданные файлы в процесс сборки. Для этого рекомендуется добавить новый файл в элементы Content или None до целевого объекта BeforeBuild, как показано в следующем примере:

<Target Name="MyTarget" BeforeTargets="BeforeBuild">
  
  <!-- Some logic that generates your file goes here -->
  <!-- Generated files should be placed in $(IntermediateOutputPath) -->

  <ItemGroup>
    <!-- If your generated file was placed in `obj\` -->
    <None Include="$(IntermediateOutputPath)my-generated-file.xyz" CopyToOutputDirectory="PreserveNewest"/>
    <!-- If you know exactly where that file is going to be, you can hard code the path. -->
    <None Include="some\specific\path\my-generated-file.xyz" CopyToOutputDirectory="PreserveNewest"/>
    
    <!-- If you want to capture "all files of a certain type", you can glob like so. -->
    <None Include="some\specific\path\*.xyz" CopyToOutputDirectory="PreserveNewest"/>
    <None Include="some\specific\path\*.*" CopyToOutputDirectory="PreserveNewest"/>
  </ItemGroup>
</Target>

Достаточно добавить созданный файл в None или Content, чтобы процесс сборки его увидел. Также следует убедиться, что он будет добавлен в нужное время. В идеале целевой объект выполняется до BeforeBuild. AssignTargetPaths является еще одним возможным целевым объектом, так как это окончательная возможность изменить элементы None и Content (среди прочих), прежде чем они будут преобразованы в новые элементы. См. общие типы элементов.

Настройка сборки решения

Важно!

Подобная настройка сборки решения применяется только к сборкам из командной строки с MSBuild.exe. Она не применяется к сборкам внутри Visual Studio. По этой причине не рекомендуется размещать настройки на уровне решения. Лучшим вариантом для настройки всех проектов в решении является использование файлов Directory.Build.props и Directory.Build.targets в папке решения, как описано в других разделах этой статьи.

Когда система MSBuild выполняет сборку файла решения, она сначала внутренне преобразует его в файл проекта и затем выполняет его сборку. Созданный файл проекта импортирует before.{solutionname}.sln.targets до определения каких-либо целевых объектов и after.{solutionname}.sln.targets после их импорта, включая целевые объекты, установленные в каталоги $(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\SolutionFile\ImportBefore и $(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\SolutionFile\ImportAfter.

Например, можно определить новый целевой объект для записи настраиваемого сообщения журнала после сборки MyCustomizedSolution.sln, создав в том же каталоге файл after.MyCustomizedSolution.sln.targets, содержащий следующее:

<Project>
 <Target Name="EmitCustomMessage" AfterTargets="Build">
   <Message Importance="High" Text="The solution has completed the Build target" />
 </Target>
</Project>

Сборка решения выполняется отдельно, поэтому заданные здесь параметры не влияют на сборку проекта.

Если у вас есть много файлов решения, которые вы хотите расширить таким же образом, но не хотите записывать в папку $(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\SolutionFile\ (для чего обычно требуются повышенные разрешения), то вы можете создать файлы Directory.Solution.props и Directory.Solution.targets и поместить их в корневой путь над файлами решения, которые вы хотите расширить. Directory.Solution.props импортируется в начале сборки решения, а Directory.Solution.targets — в конце сборки решения. При сборке файла решения файлы Directory.Build.props и Directory.Build.targets не импортируются, поэтому вместо них необходимо использовать Directory.Solution.props и Directory.Solution.targets. Они неявно импортируют друг друга.

Если у вас в корневой папке имеется файл Directory.Solution.props или Directory.Solution.targets, но под этой папкой есть решение, которое вы не хотите импортировать, вы можете с помощью упомянутых выше файлов before.{solutionname}.sln.targets и after.{solutionname}.sln.targets, относящихся к решению, задать для свойств $(ImportDirectorySolutionProps) и $(ImportDirectorySolutionTargets) значение false. Кроме того, можно использовать свойства $(DirectorySolutionPropsPath) и $(DirectorySolutionTargetsPath), чтобы указать другое расположение для этих файлов. Это может быть полезно, если у вас есть различные подмножества решений, требующие определенных значений свойств или целевых объектов, общих для подмножеств.

Настройка всех сборок .NET

При обслуживании сервера сборки может потребоваться глобально настроить параметры MSBuild для всех сборок на сервере. В принципе, вы можете изменить глобальные файлы Microsoft.Common.Targets или Microsoft.Common.Props, однако существует лучший способ. Вы можете оказать влияние на все сборки определенного типа проекта (например, все проекты C#), используя определенные свойства MSBuild и добавив определенные пользовательские файлы .targets и .props.

Чтобы повлиять на все сборки C# или Visual Basic, управляемые при установке MSBuild или Visual Studio, создайте файл Custom.Before.Microsoft.Common.Targets или Custom.After.Microsoft.Common.Targets с целевыми объектами, которые будут выполняться до или после Microsoft.Common.targets или файл Custom.Before.Microsoft.Common.Props или Custom.After.Microsoft.Common.Props со свойствами, которые будут обработаны до или после Microsoft.Common.props.

Расположение этих файлов можно указать с помощью следующих свойств MSBuild:

  • CustomBeforeMicrosoftCommonProps
  • CustomBeforeMicrosoftCommonTargets
  • CustomAfterMicrosoftCommonProps
  • CustomAfterMicrosoftCommonTargets
  • CustomBeforeMicrosoftCSharpTargets
  • CustomBeforeMicrosoftVisualBasicTargets
  • CustomAfterMicrosoftCSharpTargets
  • CustomAfterMicrosoftVisualBasicTargets

Обычные версии этих свойств влияют как на проекты C#, так и на Visual Basic. Эти свойства можно задать в командной строке MSBuild.

msbuild /p:CustomBeforeMicrosoftCommonTargets="C:\build\config\Custom.Before.Microsoft.Common.Targets" MyProject.csproj

Вы также можете использовать свойство AlternateCommonProps , чтобы указать файл для использования вместо Microsoft.Common.props. Если вы когда-либо почувствовали необходимость настроить Microsoft.Common.props , изменив его напрямую, вы можете скопировать стандартную версию этого файла в другой файл, а затем внести изменения только в альтернативную версию.

Наилучший подход зависит от вашего сценария. Благодаря расширяемости Visual Studio вы можете настроить систему сборки и реализовать механизм для установки настроек и управления ими.

Если у вас есть выделенный сервер сборки и вы хотите убедиться, что определенные целевые объекты всегда будут выполняться во всех сборках соответствующего типа проекта, выполняемого на этом сервере, то целесообразно использовать глобальный пользовательский файл .targets или .props. Если необходимо, чтобы пользовательские целевые объекты выполнялись только при определенных условиях, используйте другое расположение файла и установите путь к этому файлу, задав соответствующее свойство MSBuild в командной строке MSBuild.

Предупреждение

Visual Studio использует пользовательские файлы .targets или .props, если находит их в папке MSBuild при сборке любого проекта соответствующего типа. Это может иметь непредвиденные последствия и при неправильном завершении может отключить функцию построения на компьютере для Visual Studio.

Настройка сборок C++

В проектах C++ рассматриваемые выше файлы с расширениями .targets и .props нельзя использовать для переопределения параметров по умолчанию тем же способом. Файл Directory.Build.props импортируется файлом Microsoft.Common.props, который импортируется в Microsoft.Cpp.Default.props. При этом большинство параметров по умолчанию определяется в файле Microsoft.Cpp.props и для некоторых свойств нельзя использовать условие "если еще не определено", поскольку такие свойства уже определены, а для конкретных свойств проекта, определенных в PropertyGroup с Label="Configuration", значения по умолчанию должны отличаться (см. описание структуры файлов с расширением .vcxproj и .props).

Но вы можете задать автоматический импорт файлов .props до или после файлов Microsoft.Cpp.* с помощью следующих свойств:

  • ForceImportAfterCppDefaultProps
  • ForceImportBeforeCppProps
  • ForceImportAfterCppProps
  • ForceImportBeforeCppTargets
  • ForceImportAfterCppTargets

Чтобы настроить значения свойств по умолчанию для всех сборок C++, необходимо создать еще один файл .props (например, MyProps.props) и определить свойство ForceImportAfterCppProps в файле Directory.Build.props, указывающем на него.

<PropertyGroup>
  <ForceImportAfterCppProps>$(MsbuildThisFileDirectory)\MyProps.props</ForceImportAfterCppProps>
</PropertyGroup>

Файл MyProps.props будет автоматически импортироваться после файла Microsoft.Cpp.props.

Настройка всех сборок C++

Настраивать установку Visual Studio не рекомендуется, поскольку отслеживать вносимые при этом изменения достаточно сложно. Тем не менее, если вы расширяете возможности Visual Studio, чтобы настроить сборки C++ для конкретных платформ, можно создать файлы .targets для каждой платформы и поместить их в соответствующие папки импорта в рамках расширения Visual Studio.

Файл .targets для платформы Win32 Microsoft.Cpp.Win32.targets содержит следующий элемент Import.

<Import Project="$(VCTargetsPath)\Platforms\Win32\ImportBefore\*.targets"
        Condition="Exists('$(VCTargetsPath)\Platforms\Win32\ImportBefore')"
/>

Рядом с концом того же файла находится похожий элемент.

<Import Project="$(VCTargetsPath)\Platforms\Win32\ImportAfter\*.targets"
        Condition="Exists('$(VCTargetsPath)\Platforms\Win32\ImportAfter')"
/>

Аналогичные элементы импорта существуют и для других целевых платформ в *%ProgramFiles32%\MSBuild\Microsoft.Cpp\v{version}\Platforms*.

После того как вы поместили файл .targets в соответствующую папку ImportAfter для платформы, MSBuild импортирует ваш файл в каждую сборку C++ для этой платформы. При необходимости в нем можно разместить несколько файлов .targets.

Благодаря расширяемости Visual Studio вы можете выполнять дополнительные настройки, например определять новую платформу. Дополнительные сведения см. в статье Расширяемость проектов C++.

Указание пользовательского импорта в командной строке

Для пользовательских .targets, которые необходимо включить в определенную сборку проекта C++, установите одно или оба свойства ForceImportBeforeCppTargets и ForceImportAfterCppTargets в командной строке.

msbuild /p:ForceImportBeforeCppTargets="C:\build\config\Custom.Before.Microsoft.Cpp.Targets" MyCppProject.vcxproj

Для глобальных параметров (например, для влияния на все сборки C++ для платформы на сервере сборки) существует два метода. Во-первых, эти свойства можно задать с помощью системной переменной среды, которая всегда задается. Это работает потому, что MSBuild всегда считывает среду и создает (или переопределяет) свойства для всех переменных среды.

Пример

В этом примере показано использование предварительно обработанных выходных данных файла проекта MSBuild, чтобы определить, где следует задать свойство.

Чтобы помочь вам проанализировать использование определенного свойства, которое вы хотите задать, можно запустить MSBuild с аргументом /preprocess или /pp . Выходной текст является результатом всех операций импорта, включая системные импорты, такие как Microsoft.Common.props , которые неявно импортированы, и любой из ваших собственных импортов. С помощью этих выходных данных можно увидеть, где необходимо задать свойство относительно того, где используется его значение.

Например, предположим, что у вас есть простой проект консольного приложения .NET Core или .NET 5 или более поздней версии и вы хотите настроить промежуточную выходную папку, обычно obj. Свойство, указывающее этот путь, имеет значение BaseIntermediateOutput. Если вы попытаетесь поместить его в PropertyGroup элемент в файле проекта вместе с другими свойствами, которые уже заданы там, например TargetFramework, при сборке проекта вы обнаружите, что свойство не вступают в силу. Если вы запускаете MSBuild с параметром /pp и ищете в выходных BaseIntermediateOutputPathданных значение , вы можете понять, почему. В этом случае BaseIntermediateOutput считывается и используется в Microsoft.Common.props.

В Microsoft.Common.props есть комментарий, который говорит, что свойство BaseIntermediateOutput должно быть задано здесь, прежде чем оно будет использоваться другим свойством , MSBuildProjectExtensionsPath. Вы также можете увидеть, что при BaseIntermediateOutputPath первоначальном установке выполняется проверка существующего значения, а если оно не определено, ему присваивается значение obj.

<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">obj\</BaseIntermediateOutputPath>

Таким образом, это говорит о том, что для задания этого свойства оно должно быть указано где-то раньше, чем это. Непосредственно перед этим кодом в предварительно обработанных выходных данных вы увидите, что Directory.Build.props импортирован, поэтому это говорит о том, что его можно задать BaseIntermediateOutputPath , и он будет задан достаточно рано для получения желаемого эффекта.

Следующие сокращенные предварительно обработанные выходные данные показывают результат помещения BaseIntermediateOutput параметра в Directory.Build.props. Комментарии в верхней части стандартных импортов содержат имя файла и, как правило, некоторые полезные сведения о том, почему этот файл импортируется.

<?xml version="1.0" encoding="IBM437"?>
<!--
============================================================================================================================================
c:\source\repos\ConsoleApp9\ConsoleApp9\ConsoleApp9.csproj
============================================================================================================================================
-->
<Project DefaultTargets="Build">
  <!--
============================================================================================================================================
  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk">
  This import was added implicitly because the Project element's Sdk attribute specified "Microsoft.NET.Sdk".

C:\Program Files\dotnet\sdk\7.0.200-preview.22628.1\Sdks\Microsoft.NET.Sdk\Sdk\Sdk.props
============================================================================================================================================
-->
  <!--
***********************************************************************************************
Sdk.props

WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
          created a backup copy.  Incorrect changes to this file will make it
          impossible to load or build your projects from the command-line or the IDE.

Copyright (c) .NET Foundation. All rights reserved.
***********************************************************************************************
-->
  <PropertyGroup xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <!--
      Indicate to other targets that Microsoft.NET.Sdk is being used.

      This must be set here (as early as possible, before Microsoft.Common.props)
      so that everything that follows can depend on it.

      In particular, Directory.Build.props and nuget package props need to be able
      to use this flag and they are imported by Microsoft.Common.props.
    -->
    <UsingMicrosoftNETSdk>true</UsingMicrosoftNETSdk>
    <!--
      Indicate whether the set of SDK defaults that makes SDK style project concise are being used.
      For example: globbing, importing msbuild common targets.

      Similar to the property above, it must be set here.
    -->
    <UsingNETSdkDefaults>true</UsingNETSdkDefaults>
  </PropertyGroup>
  <PropertyGroup Condition="'$(MSBuildProjectFullPath)' == '$(ProjectToOverrideProjectExtensionsPath)'" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <MSBuildProjectExtensionsPath>$(ProjectExtensionsPathForSpecifiedProject)</MSBuildProjectExtensionsPath>
  </PropertyGroup>
  <!--<Import Project="$(AlternateCommonProps)" Condition="'$(AlternateCommonProps)' != ''" />-->
  <!--
============================================================================================================================================
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="'$(AlternateCommonProps)' == ''">

C:\Program Files\Microsoft Visual Studio\2022\Preview\MSBuild\Current\Microsoft.Common.props
============================================================================================================================================
-->
  <!--
***********************************************************************************************
Microsoft.Common.props

WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
          created a backup copy.  Incorrect changes to this file will make it
          impossible to load or build your projects from the command-line or the IDE.

Copyright (C) Microsoft Corporation. All rights reserved.
***********************************************************************************************
-->
  <PropertyGroup>
    <ImportByWildcardBeforeMicrosoftCommonProps Condition="'$(ImportByWildcardBeforeMicrosoftCommonProps)' == ''">true</ImportByWildcardBeforeMicrosoftCommonProps>
    <ImportByWildcardAfterMicrosoftCommonProps Condition="'$(ImportByWildcardAfterMicrosoftCommonProps)' == ''">true</ImportByWildcardAfterMicrosoftCommonProps>
    <ImportUserLocationsByWildcardBeforeMicrosoftCommonProps Condition="'$(ImportUserLocationsByWildcardBeforeMicrosoftCommonProps)' == ''">true</ImportUserLocationsByWildcardBeforeMicrosoftCommonProps>
    <ImportUserLocationsByWildcardAfterMicrosoftCommonProps Condition="'$(ImportUserLocationsByWildcardAfterMicrosoftCommonProps)' == ''">true</ImportUserLocationsByWildcardAfterMicrosoftCommonProps>
    <ImportDirectoryBuildProps Condition="'$(ImportDirectoryBuildProps)' == ''">true</ImportDirectoryBuildProps>
  </PropertyGroup>
  <!--
      Determine the path to the directory build props file if the user did not disable $(ImportDirectoryBuildProps) and
      they did not already specify an absolute path to use via $(DirectoryBuildPropsPath)
  -->
  <PropertyGroup Condition="'$(ImportDirectoryBuildProps)' == 'true' and '$(DirectoryBuildPropsPath)' == ''">
    <_DirectoryBuildPropsFile Condition="'$(_DirectoryBuildPropsFile)' == ''">Directory.Build.props</_DirectoryBuildPropsFile>
    <_DirectoryBuildPropsBasePath Condition="'$(_DirectoryBuildPropsBasePath)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), '$(_DirectoryBuildPropsFile)'))</_DirectoryBuildPropsBasePath>
    <DirectoryBuildPropsPath Condition="'$(_DirectoryBuildPropsBasePath)' != '' and '$(_DirectoryBuildPropsFile)' != ''">$([System.IO.Path]::Combine('$(_DirectoryBuildPropsBasePath)', '$(_DirectoryBuildPropsFile)'))</DirectoryBuildPropsPath>
  </PropertyGroup>
  <!--
============================================================================================================================================
  <Import Project="$(DirectoryBuildPropsPath)" Condition="'$(ImportDirectoryBuildProps)' == 'true' and exists('$(DirectoryBuildPropsPath)')">

c:\source\repos\ConsoleApp9\Directory.Build.props
============================================================================================================================================
-->
  <!-- Directory.build.props
-->
  <PropertyGroup>
    <BaseIntermediateOutputPath>myBaseIntermediateOutputPath</BaseIntermediateOutputPath>
  </PropertyGroup>
  <!--
============================================================================================================================================
  </Import>

C:\Program Files\Microsoft Visual Studio\2022\Preview\MSBuild\Current\Microsoft.Common.props
============================================================================================================================================
-->
  <!--
      Prepare to import project extensions which usually come from packages.  Package management systems will create a file at:
        $(MSBuildProjectExtensionsPath)\$(MSBuildProjectFile).<SomethingUnique>.props

      Each package management system should use a unique moniker to avoid collisions.  It is a wild-card import so the package
      management system can write out multiple files but the order of the import is alphabetic because MSBuild sorts the list.
  -->
  <PropertyGroup>
    <!--
        The declaration of $(BaseIntermediateOutputPath) had to be moved up from Microsoft.Common.CurrentVersion.targets
        in order for the $(MSBuildProjectExtensionsPath) to use it as a default.
    -->
    <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">obj\</BaseIntermediateOutputPath>
    <BaseIntermediateOutputPath Condition="!HasTrailingSlash('$(BaseIntermediateOutputPath)')">$(BaseIntermediateOutputPath)\</BaseIntermediateOutputPath>
    <_InitialBaseIntermediateOutputPath>$(BaseIntermediateOutputPath)</_InitialBaseIntermediateOutputPath>
    <MSBuildProjectExtensionsPath Condition="'$(MSBuildProjectExtensionsPath)' == '' ">$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath>
    <!--
        Import paths that are relative default to be relative to the importing file.  However, since MSBuildExtensionsPath
        defaults to BaseIntermediateOutputPath we expect it to be relative to the project directory.  So if the path is relative
        it needs to be made absolute based on the project directory.
    -->
    <MSBuildProjectExtensionsPath Condition="'$([System.IO.Path]::IsPathRooted($(MSBuildProjectExtensionsPath)))' == 'false'">$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(MSBuildProjectExtensionsPath)'))</MSBuildProjectExtensionsPath>
    <MSBuildProjectExtensionsPath Condition="!HasTrailingSlash('$(MSBuildProjectExtensionsPath)')">$(MSBuildProjectExtensionsPath)\</MSBuildProjectExtensionsPath>
    <ImportProjectExtensionProps Condition="'$(ImportProjectExtensionProps)' == ''">true</ImportProjectExtensionProps>
    <_InitialMSBuildProjectExtensionsPath Condition=" '$(ImportProjectExtensionProps)' == 'true' ">$(MSBuildProjectExtensionsPath)</_InitialMSBuildProjectExtensionsPath>
  </PropertyGroup>
  ...

См. также