1+ #tool "dotnet:?package=vpk&version=0.0.1053"
2+
3+ //-------------------------------------------------------------
4+
5+ public class VelopackInstaller : IInstaller
6+ {
7+ public VelopackInstaller ( BuildContext buildContext )
8+ {
9+ BuildContext = buildContext ;
10+
11+ IsEnabled = BuildContext . BuildServer . GetVariableAsBool ( "VelopackEnabled" , false , showValue : true ) ;
12+
13+ if ( IsEnabled )
14+ {
15+ IsAvailable = IsEnabled ;
16+ }
17+ }
18+
19+ public BuildContext BuildContext { get ; private set ; }
20+
21+ public bool IsEnabled { get ; private set ; }
22+
23+ public bool IsAvailable { get ; private set ; }
24+
25+ //-------------------------------------------------------------
26+
27+ public async Task PackageAsync ( string projectName , string channel )
28+ {
29+ if ( ! IsAvailable )
30+ {
31+ BuildContext . CakeContext . Information ( "Velopack is not enabled or available, skipping integration" ) ;
32+ return ;
33+ }
34+
35+ // There are 2 flavors:
36+ //
37+ // 1: Non-grouped: /[app]/[channel] (e.g. /MyApp/alpha)
38+ // Updates will always be applied, even to new major versions
39+ //
40+ // 2: Grouped by major version: /[app]/[major_version]/[channel] (e.g. /MyApp/4/alpha)
41+ // Updates will only be applied to non-major updates. This allows manual migration to
42+ // new major versions, which is very useful when there are dependencies that need to
43+ // be updated before a new major version can be switched to.
44+ var velopackOutputRoot = System . IO . Path . Combine ( BuildContext . General . OutputRootDirectory , "velopack" , projectName ) ;
45+
46+ if ( BuildContext . Wpf . GroupUpdatesByMajorVersion )
47+ {
48+ velopackOutputRoot = System . IO . Path . Combine ( velopackOutputRoot , BuildContext . General . Version . Major ) ;
49+ }
50+
51+ velopackOutputRoot = System . IO . Path . Combine ( velopackOutputRoot , channel ) ;
52+
53+ var velopackReleasesRoot = System . IO . Path . Combine ( velopackOutputRoot , "releases" ) ;
54+
55+ BuildContext . CakeContext . LogSeparator ( $ "Packaging WPF app '{ projectName } ' using Velopack") ;
56+
57+ BuildContext . CakeContext . CreateDirectory ( velopackReleasesRoot ) ;
58+
59+ var setupSuffix = BuildContext . Installer . GetDeploymentChannelSuffix ( ) ;
60+
61+ // Velopack does not seem to support . in the names (keeping same behavior as Squirrel)
62+ var projectSlug = GetProjectSlug ( projectName , "_" ) ;
63+
64+ // Copy all files to the lib so Velopack knows what to do
65+ var appSourceDirectory = System . IO . Path . Combine ( BuildContext . General . OutputRootDirectory , projectName ) ;
66+
67+ // Note: there should be only a single target framework, but pick the highest
68+ var subDirectories = System . IO . Directory . GetDirectories ( appSourceDirectory ) ;
69+ appSourceDirectory = subDirectories . Last ( ) ;
70+
71+ // Copy deployments share to the intermediate root so we can locally create the releases
72+
73+ var releasesSourceDirectory = GetDeploymentsShareRootDirectory ( projectName , channel ) ;
74+ var releasesTargetDirectory = velopackReleasesRoot ;
75+
76+ BuildContext . CakeContext . CreateDirectory ( releasesSourceDirectory ) ;
77+ BuildContext . CakeContext . CreateDirectory ( releasesTargetDirectory ) ;
78+
79+ BuildContext . CakeContext . Information ( $ "Copying releases from '{ releasesSourceDirectory } ' => '{ releasesTargetDirectory } '") ;
80+
81+ BuildContext . CakeContext . CopyDirectory ( releasesSourceDirectory , releasesTargetDirectory ) ;
82+
83+ BuildContext . CakeContext . Information ( "Generating Velopack packages, this can take a while, especially when signing is enabled..." ) ;
84+
85+ // Pack using velopack (example command line: vpk pack -u YourAppId -v 1.0.0 -p publish -e yourMainBinary.exe)
86+
87+ var appId = $ "{ projectSlug } { setupSuffix } ";
88+
89+ var argumentBuilder = new ProcessArgumentBuilder ( )
90+ . Append ( "pack" )
91+ . Append ( "--verbose" )
92+ . AppendSwitch ( "--packId" , appId )
93+ . AppendSwitch ( "--packVersion" , BuildContext . General . Version . NuGet )
94+ . AppendSwitch ( "--packDir" , appSourceDirectory )
95+ . AppendSwitch ( "--packAuthors" , BuildContext . General . Copyright . Company )
96+ . AppendSwitch ( "--delta" , "BestSpeed" )
97+ . AppendSwitch ( "--outputDir" , velopackReleasesRoot ) ;
98+
99+ // TODO: Consider adding splash image
100+
101+ // Note: this is not really generic, but this is where we store our icons file, we can
102+ // always change this in the future
103+ var iconFileName = System . IO . Path . Combine ( "." , "design" , "logo" , $ "logo{ setupSuffix } .ico") ;
104+ argumentBuilder = argumentBuilder
105+ . AppendSwitch ( "--icon" , iconFileName ) ;
106+
107+ // --signTemplate {{file}} will be substituted
108+ // Note that we need to replace / by \ on Windows
109+ var signToolExe = GetSignToolFileName ( BuildContext ) . Replace ( "/" , "\\ " ) ;
110+ var signToolCommandLine = GetSignToolCommandLine ( BuildContext ) ;
111+ if ( ! string . IsNullOrWhiteSpace ( signToolExe ) &&
112+ ! string . IsNullOrWhiteSpace ( signToolCommandLine ) )
113+ {
114+ // In order to work around a double quote issue (C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe),
115+ // if 'signtool.exe' is used, use signParams instead
116+ if ( signToolExe . EndsWith ( "\\ signtool.exe" ) )
117+ {
118+ if ( signToolCommandLine . StartsWith ( "sign " ) )
119+ {
120+ signToolCommandLine = signToolCommandLine . Substring ( "sign " . Length ) ;
121+ }
122+
123+ argumentBuilder = argumentBuilder
124+ . AppendSwitch ( "--signParams" , $ "\" { signToolCommandLine } \" ") ;
125+ }
126+ else
127+ {
128+ argumentBuilder = argumentBuilder
129+ . AppendSwitch ( "--signTemplate" , $ "\" { signToolExe } { signToolCommandLine } {{{{file}}}}\" ") ;
130+ }
131+ }
132+
133+ var vpkToolExe = BuildContext . CakeContext . Tools . Resolve ( "vpk.exe" ) ;
134+
135+ var vpkToolExitCode = BuildContext . CakeContext . StartProcess ( vpkToolExe ,
136+ new ProcessSettings
137+ {
138+ Arguments = argumentBuilder
139+ }
140+ ) ;
141+
142+ if ( vpkToolExitCode != 0 )
143+ {
144+ throw new Exception ( "Failed to pack application" ) ;
145+ }
146+
147+ // Copy setup
148+ BuildContext . CakeContext . CopyFile ( System . IO . Path . Combine ( velopackReleasesRoot , $ "{ appId } -win-Setup.exe") , System . IO . Path . Combine ( velopackReleasesRoot , "Setup.exe" ) ) ;
149+
150+ if ( BuildContext . Wpf . UpdateDeploymentsShare )
151+ {
152+ BuildContext . CakeContext . Information ( $ "Copying updated Velopack files back to deployments share at '{ releasesSourceDirectory } '") ;
153+
154+ // Copy the following files:
155+ // - [version]-delta.nupkg
156+ // - [version]-full.nupkg
157+ // - Setup.exe => Setup.exe & WpfApp.exe
158+ // - releases.win.json
159+ // - RELEASES
160+
161+ // Note to consider in future: this stores (and uploads) the same file 4 times. Maybe we need to stop processing so many files
162+ // to save time on uploads (and eventually money on storage)
163+ var velopackFiles = BuildContext . CakeContext . GetFiles ( $ "{ velopackReleasesRoot } /{ appId } -{ BuildContext . General . Version . NuGet } *.nupkg") ;
164+ BuildContext . CakeContext . CopyFiles ( velopackFiles , releasesSourceDirectory ) ;
165+ BuildContext . CakeContext . CopyFile ( System . IO . Path . Combine ( velopackReleasesRoot , $ "{ appId } -win-Portable.exe") , System . IO . Path . Combine ( releasesSourceDirectory , $ "{ appId } -win-Portable.exe") ) ;
166+ BuildContext . CakeContext . CopyFile ( System . IO . Path . Combine ( velopackReleasesRoot , $ "{ appId } -win-Setup.exe") , System . IO . Path . Combine ( releasesSourceDirectory , $ "{ appId } -win-Setup.exe") ) ;
167+ BuildContext . CakeContext . CopyFile ( System . IO . Path . Combine ( velopackReleasesRoot , "Setup.exe" ) , System . IO . Path . Combine ( releasesSourceDirectory , "Setup.exe" ) ) ;
168+ BuildContext . CakeContext . CopyFile ( System . IO . Path . Combine ( velopackReleasesRoot , "Setup.exe" ) , System . IO . Path . Combine ( releasesSourceDirectory , $ "{ projectName } .exe") ) ;
169+ BuildContext . CakeContext . CopyFile ( System . IO . Path . Combine ( velopackReleasesRoot , "releases.win.json" ) , System . IO . Path . Combine ( releasesSourceDirectory , "releases.win.json" ) ) ;
170+
171+ // Note: RELEASES is there for backwards compatibility
172+ BuildContext . CakeContext . CopyFile ( System . IO . Path . Combine ( velopackReleasesRoot , "RELEASES" ) , System . IO . Path . Combine ( releasesSourceDirectory , "RELEASES" ) ) ;
173+ }
174+ }
175+
176+ //-------------------------------------------------------------
177+
178+ public async Task < DeploymentTarget > GenerateDeploymentTargetAsync ( string projectName )
179+ {
180+ var deploymentTarget = new DeploymentTarget
181+ {
182+ Name = "Velopack"
183+ } ;
184+
185+ var channels = new [ ]
186+ {
187+ "alpha" ,
188+ "beta" ,
189+ "stable"
190+ } ;
191+
192+ var deploymentGroupNames = new List < string > ( ) ;
193+ var projectDeploymentShare = BuildContext . Wpf . GetDeploymentShareForProject ( projectName ) ;
194+
195+ if ( BuildContext . Wpf . GroupUpdatesByMajorVersion )
196+ {
197+ // Check every directory that we can parse as number
198+ var directories = System . IO . Directory . GetDirectories ( projectDeploymentShare ) ;
199+
200+ foreach ( var directory in directories )
201+ {
202+ var deploymentGroupName = new System . IO . DirectoryInfo ( directory ) . Name ;
203+
204+ if ( int . TryParse ( deploymentGroupName , out _ ) )
205+ {
206+ deploymentGroupNames . Add ( deploymentGroupName ) ;
207+ }
208+ }
209+ }
210+ else
211+ {
212+ // Just a single group
213+ deploymentGroupNames . Add ( "all" ) ;
214+ }
215+
216+ foreach ( var deploymentGroupName in deploymentGroupNames )
217+ {
218+ BuildContext . CakeContext . Information ( $ "Searching for releases for deployment group '{ deploymentGroupName } '") ;
219+
220+ var deploymentGroup = new DeploymentGroup
221+ {
222+ Name = deploymentGroupName
223+ } ;
224+
225+ var version = deploymentGroupName ;
226+ if ( version == "all" )
227+ {
228+ version = string . Empty ;
229+ }
230+
231+ foreach ( var channel in channels )
232+ {
233+ BuildContext . CakeContext . Information ( $ "Searching for releases for deployment channel '{ deploymentGroupName } /{ channel } '") ;
234+
235+ var deploymentChannel = new DeploymentChannel
236+ {
237+ Name = channel
238+ } ;
239+
240+ var targetDirectory = GetDeploymentsShareRootDirectory ( projectName , channel , version ) ;
241+
242+ BuildContext . CakeContext . Information ( $ "Searching for release files in '{ targetDirectory } '") ;
243+
244+ var fullNupkgFiles = System . IO . Directory . GetFiles ( targetDirectory , "*-full.nupkg" ) ;
245+
246+ foreach ( var fullNupkgFile in fullNupkgFiles )
247+ {
248+ BuildContext . CakeContext . Information ( $ "Applying release based on '{ fullNupkgFile } '") ;
249+
250+ var fullReleaseFileInfo = new System . IO . FileInfo ( fullNupkgFile ) ;
251+ var fullRelativeFileName = new DirectoryPath ( projectDeploymentShare ) . GetRelativePath ( new FilePath ( fullReleaseFileInfo . FullName ) ) . FullPath . Replace ( "\\ " , "/" ) ;
252+
253+ var releaseVersion = fullReleaseFileInfo . Name
254+ . Replace ( $ "{ projectName } _{ channel } -", string . Empty )
255+ . Replace ( $ "-full.nupkg", string . Empty ) ;
256+
257+ // Exception for full releases, they don't contain the channel name
258+ if ( channel == "stable" )
259+ {
260+ releaseVersion = releaseVersion . Replace ( $ "{ projectName } -", string . Empty ) ;
261+ }
262+
263+ var release = new DeploymentRelease
264+ {
265+ Name = releaseVersion ,
266+ Timestamp = fullReleaseFileInfo . CreationTimeUtc
267+ } ;
268+
269+ // Full release
270+ release . Full = new DeploymentReleasePart
271+ {
272+ RelativeFileName = fullRelativeFileName ,
273+ Size = ( ulong ) fullReleaseFileInfo . Length
274+ } ;
275+
276+ // Delta release
277+ var deltaNupkgFile = fullNupkgFile . Replace ( "-full.nupkg" , "-delta.nupkg" ) ;
278+ if ( System . IO . File . Exists ( deltaNupkgFile ) )
279+ {
280+ var deltaReleaseFileInfo = new System . IO . FileInfo ( deltaNupkgFile ) ;
281+ var deltafullRelativeFileName = new DirectoryPath ( projectDeploymentShare ) . GetRelativePath ( new FilePath ( deltaReleaseFileInfo . FullName ) ) . FullPath . Replace ( "\\ " , "/" ) ;
282+
283+ release . Delta = new DeploymentReleasePart
284+ {
285+ RelativeFileName = deltafullRelativeFileName ,
286+ Size = ( ulong ) deltaReleaseFileInfo . Length
287+ } ;
288+ }
289+
290+ deploymentChannel . Releases . Add ( release ) ;
291+ }
292+
293+ deploymentGroup . Channels . Add ( deploymentChannel ) ;
294+ }
295+
296+ deploymentTarget . Groups . Add ( deploymentGroup ) ;
297+ }
298+
299+ return deploymentTarget ;
300+ }
301+
302+ //-------------------------------------------------------------
303+
304+ private string GetDeploymentsShareRootDirectory ( string projectName , string channel )
305+ {
306+ var version = string . Empty ;
307+
308+ if ( BuildContext . Wpf . GroupUpdatesByMajorVersion )
309+ {
310+ version = BuildContext . General . Version . Major ;
311+ }
312+
313+ return GetDeploymentsShareRootDirectory ( projectName , channel , version ) ;
314+ }
315+
316+ //-------------------------------------------------------------
317+
318+ private string GetDeploymentsShareRootDirectory ( string projectName , string channel , string version )
319+ {
320+ var deploymentShare = BuildContext . Wpf . GetDeploymentShareForProject ( projectName ) ;
321+
322+ if ( ! string . IsNullOrWhiteSpace ( version ) )
323+ {
324+ deploymentShare = System . IO . Path . Combine ( deploymentShare , version ) ;
325+ }
326+
327+ var installersOnDeploymentsShare = System . IO . Path . Combine ( deploymentShare , channel ) ;
328+ BuildContext . CakeContext . CreateDirectory ( installersOnDeploymentsShare ) ;
329+
330+ return installersOnDeploymentsShare ;
331+ }
332+ }
0 commit comments