55import subprocess
66import shutil
77import stat
8+ from collections import namedtuple
9+ import fnmatch
810
911from PIL import Image
1012import requests
1517ICON_SIZES = [16 , 32 , 48 , 64 , 96 , 128 , 256 ]
1618
1719TEMP_DIR = "tmp"
18- BUILD_DIR = "build"
19- SRC_DIR = "src"
20- ARTIFACT_DIR = "artifacts"
2120
2221TOOLS_7ZIP = "./tools/7z"
2322TOOLS_NSIS = "D:/Software/NSIS/Bin/makensis.exe"
2423
24+ Version = namedtuple ('Version' , ['major' , 'minor' , 'build' ])
25+ Version .__str__ = lambda self : "{self.major}.{self.minor}.{self.build}" .format (self = self )
26+
27+ @click .group ()
28+ @click .version_option ()
29+ def cli ():
30+ pass
31+
32+
2533def download_file (url , target = None , verbose = False ):
2634 # https://stackoverflow.com/a/16696317
2735 # NOTE the stream=True parameter
@@ -141,37 +149,40 @@ def checksum_file(path, checksum_type=CHECKSUM_TYPE):
141149 data = fh .read (1_048_576 )
142150 return cs .hexdigest ()
143151
144- def cleanBuild ():
152+ def cleanBuild (builddir ):
145153 print ("Cleaning build directory..." )
146- if os .path .exists (BUILD_DIR ):
147- for path , _dirs , files in os .walk (BUILD_DIR ):
154+ if os .path .exists (builddir ):
155+ for path , _dirs , files in os .walk (builddir ):
148156 for name in files :
149157 pathname = os .path .join (path , name )
150158 # Silly git readonly files.
151159 os .chmod (pathname , stat .S_IWRITE )
152160 os .unlink (pathname )
153- shutil .rmtree (BUILD_DIR , onerror = lambda func , path , exec_info : print ("WARNING: Failed to delete " , path , exec_info ))
161+ shutil .rmtree (builddir , onerror = lambda func , path , exec_info : print ("WARNING: Failed to delete " , path , exec_info ))
154162
155- def cleanArtifacts ():
163+ def cleanArtifacts (artifactdir ):
156164 print ("Cleaning artifact directory..." )
157- shutil .rmtree ("artifacts" , onerror = lambda func , path , exec_info : print ("WARNING: Failed to delete " , path , exec_info ))
165+ if os .path .exists ("artifactdir" ):
166+ shutil .rmtree (artifactdir , onerror = lambda func , path , exec_info : print ("WARNING: Failed to delete " , path , exec_info ))
158167
159- def copySrc ():
168+ def copySrc (src_dir , build_dir ):
160169 print ("Copying src/*..." )
161- for name in os .listdir (SRC_DIR ):
162- src = os .path .join (SRC_DIR , name )
170+ for name in os .listdir (src_dir ):
171+ src = os .path .join (src_dir , name )
163172 if os .path .isdir (src ):
164- shutil .copytree (src , os .path .join (BUILD_DIR , name ))
173+ shutil .copytree (src , os .path .join (build_dir , name ))
165174 else :
166- shutil .copy (src , BUILD_DIR )
175+ shutil .copy (src , build_dir )
167176
168- def makeIco (logo ):
177+ def makeIco (icon , name , build_dir ):
169178 "Generates .ico file from .png"
170179 print ("Generating .ico file..." )
171- im = Image .open (logo )
172- im .save (os .path .join (BUILD_DIR , "ahorn.ico" ), sizes = [(x ,x ) for x in ICON_SIZES ])
180+ im = Image .open (icon )
181+ fn = "{name}.ico" .format (name )
182+ im .save (os .path .join (build_dir , fn ), sizes = [(x ,x ) for x in ICON_SIZES ])
183+ return fn
173184
174- def compileNSISTemplate ():
185+ def compileNSISTemplate (build_dir , artifact_dir , executables , ** kwargs ):
175186 "Generates NSIS script from jinja2 template"
176187 print ("Generating NSIS script..." )
177188 import jinja2
@@ -182,47 +193,119 @@ def compileNSISTemplate():
182193
183194 install_files = []
184195 install_dirs = []
185- for path , dirs , files in os .walk (BUILD_DIR ):
196+ install_size = 0
197+ install_executables = []
198+ for path , dirs , files in os .walk (build_dir ):
186199 for name in files :
187200 itempath = os .path .join (path , name )
201+ outpath = os .path .relpath (itempath , build_dir )
202+
188203 install_files .append (dict (
189204 input = itempath ,
190- output = os . path . relpath ( itempath , BUILD_DIR ) ,
205+ output = outpath ,
191206 ))
207+
208+ install_size += os .stat (itempath ).st_size
209+
210+ for pat in executables :
211+ if fnmatch .fnmatch (outpath , pat ) and outpath not in install_executables :
212+ install_executables .append (outpath )
192213
193214 for name in dirs :
194215 itempath = os .path .join (path , name )
195- relitempath = os .path .relpath (itempath , BUILD_DIR )
216+ relitempath = os .path .relpath (itempath , build_dir )
196217 install_dirs .append (relitempath )
197218
198219 template_variables = {
199220 'files' : install_files ,
200221 'dirs' : install_dirs ,
201- 'outfile' : os .path .join (ARTIFACT_DIR , "setup-${APPNAME}-${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}.exe" ),
222+ 'outfile' : os .path .join (artifact_dir , "${APPNAME}-${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}-setup.exe" ),
223+ 'size' : install_size ,
224+ 'executables' : install_executables ,
202225 }
226+ template_variables .update (kwargs )
203227
204- with open ("generic.nsi" , "w" ) as fh : # TODO: Temp file name.
228+ nsis_script = os .path .join (build_dir , "generic.nsi" )
229+ with open (nsis_script , "w" ) as fh : # TODO: Temp file name.
205230 template .stream (** template_variables ).dump (fh )
231+ return nsis_script
206232
207- def NSISBuildInstaller ():
233+ def NSISBuildInstaller (nsi_script , artifact_dir ):
208234 print ("Building NSIS installer..." )
209235
210- os .makedirs (ARTIFACT_DIR , exist_ok = True )
236+ os .makedirs (artifact_dir , exist_ok = True )
211237
212238 # http://nsis.sourceforge.net/Docs/Chapter3.html#usage
213- print (cmd ([TOOLS_NSIS , "/INPUTCHARSET" , "UTF8" , "/P3" , "/V3" , "generic.nsi" ], stdout = sys .stdout , stderr = sys .stderr , encoding = "utf8" ))
214-
215-
216- @click .command ()
217- def main ():
218- print (cleanArtifacts ())
219- print (cleanBuild ())
220- #print(copySrc())
221- #print(makeIco("fixme.png"))
239+ command = [TOOLS_NSIS , "/NOCD" , "/INPUTCHARSET" , "UTF8" , "/P3" , "/V3" , nsi_script ]
240+ print (cmd (command , stdout = sys .stdout , stderr = sys .stderr , encoding = "utf8" ))
241+
242+ @cli .command ()
243+ @click .option ('--build-dir' , default = "build" , type = click .Path (file_okay = False ))
244+ @click .option ('--artifact-dir' , default = "artifacts" , type = click .Path (file_okay = False ))
245+ @click .option ('--clean-artifacts/--no-clean-artifacts' , default = False )
246+ def clean (build_dir , artifact_dir , clean_artifacts , ** kwargs ):
247+ if clean_artifacts :
248+ cleanArtifacts (artifact_dir )
249+ cleanBuild (build_dir )
250+
251+ def validate_version (ctx , param , value ):
252+ try :
253+ return Version (* [int (x ) if x else 0 for x in value .split ('.' )])
254+ except :
255+ raise click .BadParameter ('version must be in major.minor.build format. (1.0.0)' )
256+
257+ @cli .command ()
258+ @click .option ('--version' , callback = validate_version , default = "0.0.0" , help = "Version number of build (major.minor.build)." )
259+ @click .option ('--name' , required = True , help = "Application name." )
260+ @click .option ('--company' , required = True , help = "Company name." )
261+ @click .option ('--description' , help = "Application description." )
262+
263+ @click .option ('--license' , default = None , type = click .Path (exists = True , dir_okay = False ), help = "Path to license file (rtf or txt with CRLF line endings)." )
264+ @click .option ('--icon' , default = None , type = click .Path (exists = True , dir_okay = False ), help = "Path to image to use as icon." )
265+
266+ @click .option ('--clean/--no-clean' , 'do_clean' , default = True , help = "Clean before building (default: true)." )
267+ @click .option ('--clean-artifacts/--no-clean-artifacts' , default = False , help = "Clean artifacts (default: false)." )
268+
269+ @click .option ('--build-dir' , default = "build" , type = click .Path (file_okay = False ), help = "Path to build (temporary) directory." )
270+ @click .option ('--artifact-dir' , default = "artifacts" , type = click .Path (file_okay = False ), help = "Path to installer output directory." )
271+ @click .option ('--src-dir' , default = "src" , type = click .Path (file_okay = False , exists = True ), help = "Path to application files to create installer from." )
272+ @click .option ('--executable' , '-e' , multiple = True , default = ["*.exe" ], help = "Path of executables to create startmenu shortcuts to. Relative to src-dir. Can be passed multiple times. (default: *.exe)" )
273+
274+ @click .option ('--help-url' , help = "Help URL to display in 'Add/Remove Programs'. mailto: is allowed." )
275+ @click .option ('--update-url' , help = "Update URL to display in 'Add/Remove Programs'. mailto: is allowed." )
276+ @click .option ('--website-url' , help = "Website(about) URL to display in 'Add/Remove Programs'. mailto: is allowed." )
277+
278+ @click .pass_context
279+ def build (ctx , do_clean , version , name , company , description , license , icon , build_dir , artifact_dir , src_dir , clean_artifacts , help_url , update_url , website_url , executable ):
280+ click .echo ("Building {} v{}..." .format (name , version ))
281+ click .echo (company )
282+ click .echo (description )
283+ click .echo (license )
284+ click .echo (icon )
285+ click .echo (build_dir )
286+ click .echo (artifact_dir )
287+ click .echo (src_dir )
288+ click .echo (repr (executable ))
289+ if do_clean :
290+ ctx .forward (clean )
291+
292+ os .makedirs (build_dir , exist_ok = True )
222293
223- print (compileNSISTemplate ())
294+ print (copySrc (src_dir = src_dir , build_dir = build_dir ))
295+ if icon :
296+ icon = makeIco (icon = icon , name = name , build_dir = build_dir )
297+
298+ nsi_script = compileNSISTemplate (
299+ build_dir = build_dir , artifact_dir = artifact_dir ,
300+ executables = executable ,
301+ version = version ,
302+ icon = icon , license = license ,
303+ name = name , company = company ,
304+ description = description ,
305+ help_url = help_url , update_url = update_url , website_url = website_url ,
306+ )
224307
225- print ( NSISBuildInstaller () )
308+ NSISBuildInstaller (nsi_script = nsi_script , artifact_dir = artifact_dir )
226309
227310if __name__ == "__main__" :
228- main ( )
311+ cli ( auto_envvar_prefix = 'SYRUP' )
0 commit comments