A opção --output no nível de solução não é mais válida para comandos relacionados à compilação

No SDK 7.0.200, houve uma alteração em que a opção --output/-o não é mais aceita ao usar um arquivo de solução com os seguintes comandos:

  • build
  • clean
  • pack
  • publish
  • store
  • test
  • vstest

Isso ocorre porque a semântica da propriedade OutputPath, que é controlada pela opção --output/-o, não está bem definida para soluções. Os projetos criados dessa maneira terão sua saída colocada no mesmo diretório, gerando inconsistências e levando a uma série de problemas relatados pelo usuário.

Essa alteração foi reduzida a um nível de aviso de gravidade no SDK 7.0.201 e pack foi removido da lista de comandos afetados.

Versão introduzida

O SDK do .NET 7.0.200 foi reduzido a um aviso apenas no SDK 7.0.201.

Comportamento anterior

Anteriormente, se você especificasse --output/-o ao usar um arquivo de solução, a saída para todos os projetos criados seria colocada no diretório especificado em uma ordem indefinida e inconsistente.

Novo comportamento

A CLI dotnet apresentará erro se a opção --output/-o for usada com um arquivo de solução. Começando no SDK 7.0.201, alternativamente, será emitido um aviso e, caso dotnet pack, não será gerado um aviso ou erro.

Tipo de alteração interruptiva

Essa alteração significativa pode exigir modificações para criar scripts e pipelines de integração contínua. Como resultado, isso afeta a compatibilidade binária e de origem.

Motivo da alteração

Essa alteração foi feita porque a semântica da propriedade OutputPath, que é controlada pela opção --output/-o, não está bem definida para soluções. Os projetos criados dessa maneira terão sua saída colocada no mesmo diretório, gerando inconsistências e levando a uma série de problemas relatados pelo usuário.

Quando uma solução é criada com a opção --output, a propriedade OutputPath é definida com o mesmo valor para todos os projetos, o que significa que todos os projetos terão sua saída colocada no mesmo diretório. Dependendo da complexidade dos projetos na solução, podem ocorrer resultados diferentes e inconsistentes. Vamos ver alguns exemplos de diferentes formas de solução e como elas são afetadas por um OutputPath compartilhado.

Projeto único, TargetFramework único

Imagine uma solução que contém um único projeto direcionado a um único TargetFramework, net7.0. Nesse caso, fornecer a opção --output é equivalente a definir a propriedade OutputPath no arquivo de projeto. Durante uma compilação (ou outros comandos, mas focaremos na compilação por enquanto), todas as saídas do projeto serão colocadas no diretório especificado.

Projeto único, TargetFrameworks múltiplos

Agora imagine uma solução que contém um único projeto com vários TargetFrameworks, net6.0 e net7.0. Devido a vários direcionamentos, o projeto será compilado duas vezes, uma vez para cada TargetFramework. Para cada uma dessas compilações "internas", o OutputPath será definido com o mesmo valor e, portanto, as saídas para cada uma das compilações internas serão colocadas no mesmo diretório. Isso significa que qualquer compilação concluída por último substituirá as saídas da outra compilação e, em um sistema de compilação paralela como o MSBuild, operará por padrão, 'último' é indeterminado.

Biblioteca => Console => Teste, TargetFramework único

Agora imagine uma solução que contém um projeto de biblioteca, um de console referenciando o projeto de biblioteca e um de teste referenciando o projeto de console. Todos esses projetos têm como destino um único TargetFramework, net7.0. Nesse caso, o projeto de biblioteca será criado primeiro e, em seguida, o projeto de console será criado. O projeto de teste será criado por último e fará referência ao projeto do console. Para cada projeto criado, as saídas de cada compilação serão copiadas para o diretório especificado pelo OutputPath e, portanto, o diretório final conterá ativos dos três projetos. Essa abordagem funciona em casos de teste, mas de publicação pode resultar no envio de recursos de teste para produção.

Biblioteca => Console => Teste, TargetFrameworks múltiplos

Agora pegue a mesma cadeia de projetos e adicione uma compilação de net6.0TargetFramework a eles além da compilação de net7.0. O mesmo problema da criação de projeto único e com vários destinos ocorre – cópia inconsistente de ativos específicos do TFM para o diretório especificado.

Vários aplicativos

Até agora, examinamos cenários com um gráfico de dependência linear, mas muitas soluções podem conter vários aplicativos relacionados. Isso significa que vários aplicativos podem ser criados simultaneamente na mesma pasta de saída. Se os aplicativos incluírem um arquivo de dependência com o mesmo nome, a compilação poderá falhar intermitentemente quando vários projetos tentarem fazer a gravação simultaneamente nesse arquivo no caminho de saída.

Se vários aplicativos dependerem de diferentes versões de um arquivo, mesmo que a compilação seja bem-sucedida, a versão do arquivo que é copiada para o caminho de saída pode ser não determinística. Isso pode acontecer quando os projetos dependem (possivelmente de forma transitiva) de diferentes versões de um pacote NuGet. Em um único projeto, o NuGet ajuda a garantir que suas dependências (incluindo quaisquer dependências transitivas por meio de pacotes NuGet e/ou referências de projeto) sejam unificadas na mesma versão. Como a unificação é feita no contexto de um único projeto e seus projetos dependentes, isso significa que é possível resolver diferentes versões de um pacote quando dois projetos separados de nível superior são criados. Se o projeto que depende da versão superior copiar a dependência por último, geralmente os aplicativos serão executados com sucesso. No entanto, se a versão inferior for copiada por último, o aplicativo que foi compilado na versão superior falhará ao carregar o assembly em tempo de execução. Como a versão copiada pode ser não determinística, isso pode gerar compilações esporádicas e não confiáveis, por meio das quais o diagnóstico do problema é dificultado.

Outros exemplos

Para obter mais exemplos de como esse erro subjacente se apresenta na prática, confira a discussão em dotnet/sdk#15607.

A recomendação geral é executar a ação que você executou anteriormente sem a opção --output/-o e, em seguida, mover a saída para o local desejado após a conclusão do comando. Também é possível realizar a ação em um projeto específico e ainda aplicar a opção --output/-o, pois esta possui semântica mais bem definida.

Se você deseja manter exatamente o comportamento existente, pode usar o sinalizador --property para definir uma propriedade MSBuild para o diretório desejado. A propriedade a ser usada varia de acordo com o comando:

Comando Propriedade Exemplo
build OutputPath dotnet build --property:OutputPath=DESIRED_PATH
clean OutputPath dotnet clean --property:OutputPath=DESIRED_PATH
pack PackageOutputPath dotnet pack --property:PackageOutputPath=DESIRED_PATH
publish PublishDir dotnet publish --property:PublishDir=DESIRED_PATH
store OutputPath dotnet store --property:OutputPath=DESIRED_PATH
test TestResultsDirectory dotnet test --property:OutputPath=DESIRED_PATH

OBSERVAÇÃO para obter melhores resultados, o DESIRED_PATH deve ser um caminho absoluto. Os caminhos relativos serão "ancorados" (ou seja, tornados absolutos) de maneiras inesperadas e podem não funcionar da mesma forma com todos os comandos.