@@ -353,7 +353,7 @@ private string BuildLocalApp([CallerMemberName] string testName = "TestName", st
353353 public async Task EndToEnd_MultiProjectSolution ( )
354354 {
355355 ILogger logger = _loggerFactory . CreateLogger ( nameof ( EndToEnd_MultiProjectSolution ) ) ;
356- DirectoryInfo newSolutionDir = new ( Path . Combine ( TestSettings . TestArtifactsDirectory , $ "CreateNewImageTest_EndToEnd_MultiProjectSolution" ) ) ;
356+ DirectoryInfo newSolutionDir = new ( Path . Combine ( TestSettings . TestArtifactsDirectory , nameof ( EndToEnd_MultiProjectSolution ) ) ) ;
357357
358358 if ( newSolutionDir . Exists )
359359 {
@@ -431,6 +431,82 @@ public async Task EndToEnd_MultiProjectSolution()
431431 commandResult . Should ( ) . HaveStdOutContaining ( "Pushed image 'consoleapp:latest'" ) ;
432432 }
433433
434+ /// <summary>
435+ /// Tests that a multi-project solution with a library that targets multiple frameworks can be published.
436+ /// This is interesting because before https://github.com/dotnet/sdk/pull/47693 the container targets
437+ /// wouldn't be loaded for multi-TFM project evaluations, so any calls to the PublishContainer target
438+ /// for libraries (which may be multi-targeted even when referenced from a single-target published app project) would fail.
439+ /// It's safe to load the target for libraries in a multi-targeted context because libraries don't have EnableSdkContainerSupport
440+ /// enabled by default, so the target will be skipped.
441+ /// </summary>
442+ [ DockerAvailableFact ]
443+ public async Task EndToEnd_MultiProjectSolution_with_multitargeted_library ( )
444+ {
445+ ILogger logger = _loggerFactory . CreateLogger ( nameof ( EndToEnd_MultiProjectSolution_with_multitargeted_library ) ) ;
446+ DirectoryInfo newSolutionDir = new ( Path . Combine ( TestSettings . TestArtifactsDirectory , nameof ( EndToEnd_MultiProjectSolution_with_multitargeted_library ) ) ) ;
447+
448+ if ( newSolutionDir . Exists )
449+ {
450+ newSolutionDir . Delete ( recursive : true ) ;
451+ }
452+
453+ newSolutionDir . Create ( ) ;
454+
455+ // Create solution with projects
456+ new DotnetNewCommand ( _testOutput , "sln" , "-n" , nameof ( EndToEnd_MultiProjectSolution_with_multitargeted_library ) )
457+ . WithVirtualHive ( )
458+ . WithWorkingDirectory ( newSolutionDir . FullName )
459+ . Execute ( )
460+ . Should ( ) . Pass ( ) ;
461+
462+ new DotnetNewCommand ( _testOutput , "web" , "-n" , "WebApp" )
463+ . WithVirtualHive ( )
464+ . WithWorkingDirectory ( newSolutionDir . FullName )
465+ . Execute ( )
466+ . Should ( ) . Pass ( ) ;
467+
468+ new DotnetCommand ( _testOutput , "sln" , "add" , Path . Combine ( "WebApp" , "WebApp.csproj" ) )
469+ . WithWorkingDirectory ( newSolutionDir . FullName )
470+ . Execute ( )
471+ . Should ( ) . Pass ( ) ;
472+
473+ new DotnetNewCommand ( _testOutput , "classlib" , "-n" , "Library" )
474+ . WithVirtualHive ( )
475+ . WithWorkingDirectory ( newSolutionDir . FullName )
476+ . Execute ( )
477+ . Should ( ) . Pass ( ) ;
478+
479+ new DotnetCommand ( _testOutput , "sln" , "add" , Path . Combine ( "Library" , "Library.csproj" ) )
480+ . WithWorkingDirectory ( newSolutionDir . FullName )
481+ . Execute ( )
482+ . Should ( ) . Pass ( ) ;
483+
484+ // Set TFMs for Library - use current toolset + NS2.0 for compatibility
485+ // also set IsPublishable to false
486+ using ( FileStream stream = File . Open ( Path . Join ( newSolutionDir . FullName , "Library" , "Library.csproj" ) , FileMode . Open , FileAccess . ReadWrite ) )
487+ {
488+ XDocument document = await XDocument . LoadAsync ( stream , LoadOptions . None , CancellationToken . None ) ;
489+ var tfmNode =
490+ document
491+ . Descendants ( )
492+ . First ( e => e . Name . LocalName == "TargetFramework" ) ;
493+ var propertyGroupNode = tfmNode . Parent ! ;
494+ tfmNode . Remove ( ) ;
495+ propertyGroupNode . Add ( new XElement ( "TargetFrameworks" , $ "{ ToolsetInfo . CurrentTargetFramework } ;netstandard2.0") ) ;
496+ propertyGroupNode . Add ( new XElement ( "IsPublishable" , "false" ) ) ;
497+ stream . SetLength ( 0 ) ;
498+ await document . SaveAsync ( stream , SaveOptions . None , CancellationToken . None ) ;
499+ }
500+
501+ // Publish
502+ CommandResult commandResult = new DotnetCommand ( _testOutput , "publish" , "/t:PublishContainer" )
503+ . WithWorkingDirectory ( newSolutionDir . FullName )
504+ . Execute ( ) ;
505+
506+ commandResult . Should ( ) . Pass ( ) ;
507+ commandResult . Should ( ) . HaveStdOutContaining ( "Pushed image 'webapp:latest'" ) ;
508+ }
509+
434510 [ DockerAvailableTheory ( ) ]
435511 [ InlineData ( "webapi" , false ) ]
436512 [ InlineData ( "webapi" , true ) ]
@@ -1014,7 +1090,7 @@ public void EndToEndMultiArch_RemoteRegistry()
10141090 . And . HaveStdOutContaining ( $ "Pushed image '{ imageX64 } ' to registry '{ registry } '.")
10151091 . And . HaveStdOutContaining ( $ "Pushed image '{ imageArm64 } ' to registry '{ registry } '.")
10161092 . And . HaveStdOutContaining ( $ "Pushed image index '{ imageIndex } ' to registry '{ registry } '.") ;
1017-
1093+
10181094 // Check that the containers can be run
10191095 // First pull the image from the registry for each platform
10201096 ContainerCli . PullCommand (
@@ -1031,7 +1107,7 @@ public void EndToEndMultiArch_RemoteRegistry()
10311107 imageFromRegistry )
10321108 . Execute ( )
10331109 . Should ( ) . Pass ( ) ;
1034-
1110+
10351111 // Run the containers
10361112 ContainerCli . RunCommand (
10371113 _testOutput ,
@@ -1353,4 +1429,45 @@ static string[] DecideEntrypoint(string rid, string appName, string workingDir)
13531429 return new [ ] { $ "{ workingDir } /{ binary } " } ;
13541430 }
13551431 }
1432+
1433+ [ DockerAvailableFact ( checkContainerdStoreAvailability : true ) ]
1434+ public void EnforcesOciSchemaForMultiRIDTarballOutput ( )
1435+ {
1436+ string imageName = NewImageName ( ) ;
1437+ string tag = "1.0" ;
1438+
1439+ // Create new console app
1440+ DirectoryInfo newProjectDir = CreateNewProject ( "webapp" ) ;
1441+
1442+ // Run PublishContainer for multi-arch with ContainerGenerateLabels
1443+ var publishResult = new DotnetCommand (
1444+ _testOutput ,
1445+ "publish" ,
1446+ "/t:PublishContainer" ,
1447+ "/p:RuntimeIdentifiers=\" linux-x64;linux-arm64\" " ,
1448+ $ "/p:ContainerBaseImage={ DockerRegistryManager . FullyQualifiedBaseImageAspNet } ",
1449+ $ "/p:ContainerRepository={ imageName } ",
1450+ $ "/p:ContainerImageTag={ tag } ",
1451+ "/p:EnableSdkContainerSupport=true" ,
1452+ "/p:ContainerArchiveOutputPath=archive.tar.gz" ,
1453+ "-getProperty:GeneratedImageIndex" ,
1454+ "-getItem:GeneratedContainers" ,
1455+ "/bl" )
1456+ . WithWorkingDirectory ( newProjectDir . FullName )
1457+ . Execute ( ) ;
1458+
1459+ publishResult . Should ( ) . Pass ( ) ;
1460+ var jsonDump = JsonDocument . Parse ( publishResult . StdOut ) ;
1461+ var index = JsonDocument . Parse ( jsonDump . RootElement . GetProperty ( "Properties" ) . GetProperty ( "GeneratedImageIndex" ) . ToString ( ) ) ;
1462+ var containers = jsonDump . RootElement . GetProperty ( "Items" ) . GetProperty ( "GeneratedContainers" ) . EnumerateArray ( ) . ToArray ( ) ;
1463+
1464+ index . RootElement . GetProperty ( "mediaType" ) . GetString ( ) . Should ( ) . Be ( "application/vnd.oci.image.index.v1+json" ) ;
1465+ containers . Should ( ) . HaveCount ( 2 ) ;
1466+ foreach ( var container in containers )
1467+ {
1468+ container . GetProperty ( "ManifestMediaType" ) . GetString ( ) . Should ( ) . Be ( "application/vnd.oci.image.manifest.v1+json" ) ;
1469+ }
1470+ // Cleanup
1471+ newProjectDir . Delete ( true ) ;
1472+ }
13561473}
0 commit comments