Skip to content

Commit 636882f

Browse files
committed
Initial working version of Click commandline interface.
1 parent 93775ac commit 636882f

5 files changed

Lines changed: 213 additions & 62 deletions

File tree

.gitignore

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
tmp/
2-
build/
3-
artifacts/
4-
Ahorn.nsi
1+
tmp/
2+
build/
3+
artifacts/
4+
__pycache__/
5+
syrup.egg-info/

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Installation
2+
============
3+
```
4+
pip install git+https://git.cruor.openshell.no/celestialcartographers/syrup.git
5+
```
6+
7+
Usage
8+
=====
9+
```
10+
syrup build --name="TestApp" --company="TestingInc" --version=0.0.1 --src-dir=testinput
11+
```
12+
13+
See `syrup build --help` for details.
14+
15+
Developement
16+
============
17+
```
18+
git clone https://git.cruor.openshell.no/celestialcartographers/syrup.git
19+
pip install --editable ./syrup
20+
```

setup.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from setuptools import setup
2+
# TODO: include package data (templates)
3+
setup(
4+
name='syrup',
5+
version='0.0.1',
6+
py_modules=['syrup'],
7+
install_requires=[
8+
'click',
9+
'requests',
10+
'Pillow',
11+
],
12+
entry_points='''
13+
[console_scripts]
14+
syrup=syrup:cli
15+
''',
16+
)

syrup.py

Lines changed: 120 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import subprocess
66
import shutil
77
import stat
8+
from collections import namedtuple
9+
import fnmatch
810

911
from PIL import Image
1012
import requests
@@ -15,13 +17,19 @@
1517
ICON_SIZES = [16, 32, 48, 64, 96, 128, 256]
1618

1719
TEMP_DIR = "tmp"
18-
BUILD_DIR = "build"
19-
SRC_DIR = "src"
20-
ARTIFACT_DIR = "artifacts"
2120

2221
TOOLS_7ZIP = "./tools/7z"
2322
TOOLS_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+
2533
def 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

227310
if __name__ == "__main__":
228-
main()
311+
cli(auto_envvar_prefix='SYRUP')

0 commit comments

Comments
 (0)