11using System . IO . Compression ;
22using System . Text . Json ;
33using CSharpFunctionalExtensions ;
4- using Zafiro . DivineBytes ;
5- using IOPath = System . IO . Path ;
6- using Zafiro . FileSystem . Core ;
74
85namespace DotnetPackaging . Exe ;
96
@@ -18,183 +15,147 @@ public static async Task<Result> Build(
1815 Maybe < byte [ ] > logoBytes ,
1916 string outputPath )
2017 {
21- if ( ! File . Exists ( stubPath ) )
18+ var tempRoot = string . Empty ;
19+ try
2220 {
23- return Result . Failure ( $ "Stub not found: { stubPath } ") ;
24- }
21+ if ( ! File . Exists ( stubPath ) )
22+ {
23+ return Result . Failure ( $ "Stub not found: { stubPath } ") ;
24+ }
2525
26- if ( ! Directory . Exists ( publishDir ) )
27- {
28- return Result . Failure ( $ "Publish directory not found: { publishDir } ") ;
29- }
26+ if ( ! Directory . Exists ( publishDir ) )
27+ {
28+ return Result . Failure ( $ "Publish directory not found: { publishDir } ") ;
29+ }
3030
31- var outputDirectory = IOPath . GetDirectoryName ( outputPath ) ;
32- if ( string . IsNullOrWhiteSpace ( outputDirectory ) )
33- {
34- return Result . Failure ( "Output directory cannot be determined." ) ;
35- }
31+ var outputDirectory = Path . GetDirectoryName ( outputPath ) ;
32+ if ( string . IsNullOrWhiteSpace ( outputDirectory ) )
33+ {
34+ return Result . Failure ( "Output directory cannot be determined." ) ;
35+ }
3636
37- var publishContainerResult = BuildContainerFromDirectory ( publishDir ) ;
38- if ( publishContainerResult . IsFailure )
39- {
40- return Result . Failure ( publishContainerResult . Error ) ;
41- }
37+ Directory . CreateDirectory ( outputDirectory ) ;
4238
43- var stubSource = ByteSource . FromAsyncStreamFactory ( ( ) => Task . FromResult < Stream > ( File . OpenRead ( stubPath ) ) ) ;
44- var logoSource = logoBytes . Map ( bytes => ( IByteSource ) ByteSource . FromBytes ( bytes ) ) ;
45- var bundleResult = await Build ( stubSource , publishContainerResult . Value , metadata , logoSource ) ;
46- if ( bundleResult . IsFailure )
47- {
48- return Result . Failure ( bundleResult . Error ) ;
49- }
39+ tempRoot = Path . Combine ( Path . GetTempPath ( ) , "dp-exe-" + Guid . NewGuid ( ) ) ;
40+ Directory . CreateDirectory ( tempRoot ) ;
5041
51- Directory . CreateDirectory ( outputDirectory ) ;
52- var uninstallerPath = IOPath . Combine ( outputDirectory , "Uninstaller.exe" ) ;
53- await Persist ( bundleResult . Value . Installer , outputPath ) ;
54- await Persist ( bundleResult . Value . Uninstaller , uninstallerPath ) ;
42+ var uninstallerPayloadRoot = Path . Combine ( tempRoot , "uninstaller_payload" ) ;
43+ Directory . CreateDirectory ( uninstallerPayloadRoot ) ;
44+ await WriteMetadata ( uninstallerPayloadRoot , metadata ) ;
45+ await WriteSupportStub ( uninstallerPayloadRoot , stubPath ) ;
5546
56- return Result . Success ( ) ;
57- }
47+ var uninstallerPayloadZip = Path . Combine ( outputDirectory , "uninstaller_payload.zip" ) ;
48+ CreatePayloadZip ( uninstallerPayloadRoot , uninstallerPayloadZip ) ;
5849
59- public static async Task < Result < SimpleExeBundle > > Build (
60- IByteSource stub ,
61- IContainer publishContent ,
62- InstallerMetadata metadata ,
63- Maybe < IByteSource > logoBytes )
64- {
65- var metadataSource = Serialize ( metadata ) ;
66- var uninstallerPayloadResult = await BuildUninstallerPayload ( stub , metadataSource ) ;
67- if ( uninstallerPayloadResult . IsFailure )
68- {
69- return Result . Failure < SimpleExeBundle > ( uninstallerPayloadResult . Error ) ;
70- }
50+ var uninstallerOutput = Path . Combine ( outputDirectory , "Uninstaller.exe" ) ;
51+ PayloadAppender . AppendPayload ( stubPath , uninstallerPayloadZip , uninstallerOutput ) ;
7152
72- var uninstaller = new Resource ( "Uninstaller.exe" , PayloadAppender . AppendPayload ( stub , uninstallerPayloadResult . Value ) ) ;
53+ var installerPayloadRoot = Path . Combine ( tempRoot , "installer_payload" ) ;
54+ Directory . CreateDirectory ( installerPayloadRoot ) ;
55+ await WriteMetadata ( installerPayloadRoot , metadata ) ;
56+ CopyDirectory ( publishDir , Path . Combine ( installerPayloadRoot , "Content" ) ) ;
57+ CopySupportBinary ( uninstallerOutput , Path . Combine ( installerPayloadRoot , "Support" ) ) ;
58+ await WriteLogo ( installerPayloadRoot , logoBytes ) ;
7359
74- var installerPayloadResult = await BuildInstallerPayload ( metadataSource , publishContent , logoBytes , uninstaller ) ;
75- if ( installerPayloadResult . IsFailure )
60+ var installerPayloadZip = Path . Combine ( outputDirectory , "installer_payload.zip" ) ;
61+ CreatePayloadZip ( installerPayloadRoot , installerPayloadZip ) ;
62+
63+ PayloadAppender . AppendPayload ( stubPath , installerPayloadZip , outputPath ) ;
64+ return Result . Success ( ) ;
65+ }
66+ catch ( Exception ex )
67+ {
68+ return Result . Failure ( ex . Message ) ;
69+ }
70+ finally
7671 {
77- return Result . Failure < SimpleExeBundle > ( installerPayloadResult . Error ) ;
72+ TryDeleteTempDirectories ( ) ;
7873 }
7974
80- var installer = new Resource ( "Installer.exe" , PayloadAppender . AppendPayload ( stub , installerPayloadResult . Value ) ) ;
75+ void TryDeleteTempDirectories ( )
76+ {
77+ if ( string . IsNullOrWhiteSpace ( tempRoot ) )
78+ {
79+ return ;
80+ }
8181
82- return Result . Success ( new SimpleExeBundle ( installer , uninstaller ) ) ;
82+ try
83+ {
84+ Directory . Delete ( tempRoot , true ) ;
85+ }
86+ catch
87+ {
88+ // best effort cleanup
89+ }
90+ }
8391 }
8492
85- private static async Task Persist ( INamedByteSource artifact , string path )
93+ private static void CreatePayloadZip ( string sourceDirectory , string destinationZip )
8694 {
87- Directory . CreateDirectory ( IOPath . GetDirectoryName ( path ) ! ) ;
88- await using var input = artifact . ToStreamSeekable ( ) ;
89- await using var output = File . Create ( path ) ;
90- await input . CopyToAsync ( output ) ;
95+ Directory . CreateDirectory ( Path . GetDirectoryName ( destinationZip ) ! ) ;
96+ if ( File . Exists ( destinationZip ) )
97+ {
98+ File . Delete ( destinationZip ) ;
99+ }
100+
101+ ZipFile . CreateFromDirectory ( sourceDirectory , destinationZip , CompressionLevel . Optimal , includeBaseDirectory : false ) ;
91102 }
92103
93- private static IByteSource Serialize ( InstallerMetadata metadata )
104+ private static async Task WriteMetadata ( string destinationDirectory , InstallerMetadata meta )
94105 {
95- var json = JsonSerializer . Serialize ( metadata , new JsonSerializerOptions
106+ Directory . CreateDirectory ( destinationDirectory ) ;
107+ var metadataPath = Path . Combine ( destinationDirectory , "metadata.json" ) ;
108+ await using var stream = File . Create ( metadataPath ) ;
109+ await JsonSerializer . SerializeAsync ( stream , meta , new JsonSerializerOptions
96110 {
97111 PropertyNamingPolicy = JsonNamingPolicy . CamelCase ,
98112 WriteIndented = false
99113 } ) ;
100-
101- return ByteSource . FromString ( json ) ;
102- }
103-
104- private static async Task < Result < IByteSource > > BuildUninstallerPayload ( IByteSource stub , IByteSource metadataSource )
105- {
106- var entries = new Dictionary < string , IByteSource > ( StringComparer . Ordinal )
107- {
108- [ "metadata.json" ] = metadataSource ,
109- [ $ "Support/{ "Uninstaller.exe" } "] = stub
110- } ;
111-
112- var containerResult = entries . ToRootContainer ( ) ;
113- if ( containerResult . IsFailure )
114- {
115- return Result . Failure < IByteSource > ( containerResult . Error ) ;
116- }
117-
118- return await CreatePayloadZip ( containerResult . Value ) ;
119114 }
120115
121- private static async Task < Result < IByteSource > > BuildInstallerPayload (
122- IByteSource metadataSource ,
123- IContainer publishContent ,
124- Maybe < IByteSource > logoBytes ,
125- INamedByteSource uninstaller )
116+ private static async Task WriteLogo ( string payloadRoot , Maybe < byte [ ] > logoBytes )
126117 {
127- var entries = new Dictionary < string , IByteSource > ( StringComparer . Ordinal )
128- {
129- [ "metadata.json" ] = metadataSource ,
130- [ $ "Support/{ uninstaller . Name } "] = uninstaller
131- } ;
132-
133- foreach ( var file in publishContent . ResourcesWithPathsRecursive ( ) )
134- {
135- var entryPath = $ "Content/{ file . FullPath ( ) . ToString ( ) . Replace ( '\\ ' , '/' ) } ";
136- entries [ entryPath ] = file ;
137- }
138-
139- logoBytes . Execute ( bytes => entries [ BrandingLogoEntry ] = bytes ) ;
140-
141- var containerResult = entries . ToRootContainer ( ) ;
142- if ( containerResult . IsFailure )
143- {
144- return Result . Failure < IByteSource > ( containerResult . Error ) ;
145- }
146-
147- return await CreatePayloadZip ( containerResult . Value ) ;
118+ await logoBytes . Match (
119+ async bytes =>
120+ {
121+ var brandingDir = Path . Combine ( payloadRoot , "Branding" ) ;
122+ Directory . CreateDirectory ( brandingDir ) ;
123+ var logoPath = Path . Combine ( brandingDir , Path . GetFileName ( BrandingLogoEntry ) ) ;
124+ await File . WriteAllBytesAsync ( logoPath , bytes ) ;
125+ } ,
126+ ( ) => Task . CompletedTask ) ;
148127 }
149128
150- private static Result < RootContainer > BuildContainerFromDirectory ( string root )
129+ private static async Task WriteSupportStub ( string payloadRoot , string stubPath )
151130 {
152- try
153- {
154- var files = Directory
155- . EnumerateFiles ( root , "*" , SearchOption . AllDirectories )
156- . ToDictionary (
157- file => IOPath . GetRelativePath ( root , file ) . Replace ( "\\ " , "/" ) ,
158- file => ( IByteSource ) ByteSource . FromAsyncStreamFactory ( ( ) => Task . FromResult < Stream > ( File . OpenRead ( file ) ) ) ,
159- StringComparer . Ordinal ) ;
160-
161- return files . ToRootContainer ( ) ;
162- }
163- catch ( Exception ex )
164- {
165- return Result . Failure < RootContainer > ( $ "Failed to read directory '{ root } ': { ex . Message } ") ;
166- }
131+ var supportDir = Path . Combine ( payloadRoot , "Support" ) ;
132+ Directory . CreateDirectory ( supportDir ) ;
133+ await using var input = File . OpenRead ( stubPath ) ;
134+ await using var output = File . Create ( Path . Combine ( supportDir , "Uninstaller.exe" ) ) ;
135+ await input . CopyToAsync ( output ) ;
167136 }
168137
169- private static Task < Result < IByteSource > > CreatePayloadZip ( IContainer container )
138+ private static void CopySupportBinary ( string uninstallerPath , string supportRoot )
170139 {
171- return Task . FromResult ( Result . Success ( CreateZipSource ( container ) ) ) ;
140+ Directory . CreateDirectory ( supportRoot ) ;
141+ var destination = Path . Combine ( supportRoot , "Uninstaller.exe" ) ;
142+ File . Copy ( uninstallerPath , destination , overwrite : true ) ;
172143 }
173144
174- private static IByteSource CreateZipSource ( IContainer container )
145+ private static void CopyDirectory ( string sourceDir , string destinationDir )
175146 {
176- return ByteSource . FromAsyncStreamFactory ( async ( ) =>
147+ foreach ( var directory in Directory . EnumerateDirectories ( sourceDir , "*" , SearchOption . AllDirectories ) )
177148 {
178- var zipStream = new MemoryStream ( ) ;
179-
180- await using ( var zip = new ZipArchive ( zipStream , ZipArchiveMode . Create , leaveOpen : true ) )
181- {
182- foreach ( var resource in container . ResourcesWithPathsRecursive ( ) )
183- {
184- var entry = zip . CreateEntry ( resource . FullPath ( ) . ToString ( ) . Replace ( '\\ ' , '/' ) , CompressionLevel . Optimal ) ;
185- await using var entryStream = entry . Open ( ) ;
186- await using var resourceStream = resource . ToStreamSeekable ( ) ;
187- await resourceStream . CopyToAsync ( entryStream ) ;
188- }
189- }
149+ var relative = Path . GetRelativePath ( sourceDir , directory ) ;
150+ Directory . CreateDirectory ( Path . Combine ( destinationDir , relative ) ) ;
151+ }
190152
191- zipStream . Position = 0 ;
192- var final = new MemoryStream ( ) ;
193- await zipStream . CopyToAsync ( final ) ;
194- final . Position = 0 ;
195- return final ;
196- } ) ;
153+ foreach ( var file in Directory . EnumerateFiles ( sourceDir , "*" , SearchOption . AllDirectories ) )
154+ {
155+ var relative = Path . GetRelativePath ( sourceDir , file ) ;
156+ var destination = Path . Combine ( destinationDir , relative ) ;
157+ Directory . CreateDirectory ( Path . GetDirectoryName ( destination ) ! ) ;
158+ File . Copy ( file , destination , overwrite : true ) ;
159+ }
197160 }
198161}
199-
200- public sealed record SimpleExeBundle ( INamedByteSource Installer , INamedByteSource Uninstaller ) ;
0 commit comments