diff --git a/.gitignore b/.gitignore index 05a98150..69bd4df2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ # Vagrant .vagrant + +# Package files +*.pkg diff --git a/BUILD.md b/BUILD.md index 58020756..1cf51fec 100644 --- a/BUILD.md +++ b/BUILD.md @@ -59,3 +59,11 @@ You will need Microsoft Visual Studio 2013 to compile freelan. All projects come The root directory also contains a solution file (`.sln`) that references all the sub-projects. The resulting binaries will be located in the [install](install) directory. + +### Mac OSX + +On Mac OSX, an additional SCons target exists to build the freelan installation package: + +> scons package + +The package will be generated at the root of the repository. diff --git a/SConscript b/SConscript index 75468b42..995362ec 100644 --- a/SConscript +++ b/SConscript @@ -57,24 +57,25 @@ for x in Glob('apps/*'): samples = [] -for x in Glob('samples/*'): - libname = os.path.basename(str(x)) +if env.mode != 'retail': + for x in Glob('samples/*'): + libname = os.path.basename(str(x)) - if not sys.platform.startswith('linux'): - if libname in 'netlinkplus': - continue + if not sys.platform.startswith('linux'): + if libname in 'netlinkplus': + continue - for y in x.glob('*'): - sconscript_path = y.File('SConscript') + for y in x.glob('*'): + sconscript_path = y.File('SConscript') - if sconscript_path.exists(): - name = 'sample_%s_%s' % (libname, os.path.basename(str(y))) - sample = SConscript(sconscript_path, exports='env dirs name') - samples.extend(sample) + if sconscript_path.exists(): + name = 'sample_%s_%s' % (libname, os.path.basename(str(y))) + sample = SConscript(sconscript_path, exports='env dirs name') + samples.extend(sample) - if env.debug: - samples.extend(env.SymLink(y.File('%sd' % os.path.basename(str(y))).srcnode(), sample)) - else: - samples.extend(env.SymLink(y.File(os.path.basename(str(y))).srcnode(), sample)) + if env.mode == 'release': + samples.extend(env.SymLink(y.File('%sd' % os.path.basename(str(y))).srcnode(), sample)) + else: + samples.extend(env.SymLink(y.File(os.path.basename(str(y))).srcnode(), sample)) Return('libraries includes apps samples configurations') diff --git a/SConstruct b/SConstruct index a86c455d..3675287a 100644 --- a/SConstruct +++ b/SConstruct @@ -40,12 +40,11 @@ class FreelanEnvironment(Environment): A freelan specific environment class. """ - def __init__(self, debug, prefix, **kwargs): + def __init__(self, mode, prefix, **kwargs): """ Initialize the environment. - :param debug: A boolean value that indicates whether to set debug flags - in the environment. + :param mode: The compilation mode. :param prefix: The installation prefix. """ super(FreelanEnvironment, self).__init__(**kwargs) @@ -71,7 +70,7 @@ class FreelanEnvironment(Environment): if flag in os.environ: self[flag] = Split(os.environ[flag]) - self.debug = debug + self.mode = mode self.prefix = prefix if os.path.basename(self['CXX']) == 'clang++': @@ -98,7 +97,7 @@ class FreelanEnvironment(Environment): self.Append(CXXFLAGS=['--stdlib=libc++']) self.Append(LDFLAGS=['--stdlib=libc++']) - if self.debug: + if self.mode == 'debug': self.Append(CXXFLAGS=['-g']) self.Append(CXXFLAGS='-DFREELAN_DEBUG=1') else: @@ -151,20 +150,29 @@ mode = GetOption('mode') prefix = os.path.abspath(GetOption('prefix')) if mode in ('all', 'release'): - env = FreelanEnvironment(debug=False, prefix=prefix) - libraries, includes, apps, samples, configurations = SConscript('SConscript', exports='env', variant_dir=os.path.join('build', 'release')) + env = FreelanEnvironment(mode='release', prefix=prefix) + libraries, includes, apps, samples, configurations = SConscript('SConscript', exports='env', variant_dir=os.path.join('build', env.mode)) install = env.Install(os.path.join(prefix, 'bin'), apps) install.extend(env.Install(os.path.join(prefix, 'etc', 'freelan'), configurations)) + Alias('install', install) Alias('apps', apps) Alias('samples', samples) Alias('all', install + apps + samples) if mode in ('all', 'debug'): - env = FreelanEnvironment(debug=True, prefix=prefix) - libraries, includes, apps, samples, configurations = SConscript('SConscript', exports='env', variant_dir=os.path.join('build', 'debug')) + env = FreelanEnvironment(mode='debug', prefix=prefix) + libraries, includes, apps, samples, configurations = SConscript('SConscript', exports='env', variant_dir=os.path.join('build', env.mode)) Alias('apps', apps) Alias('samples', samples) Alias('all', apps + samples) +if sys.platform.startswith('darwin'): + retail_prefix = '/usr/local' + env = FreelanEnvironment(mode='retail', prefix=retail_prefix) + libraries, includes, apps, samples, configurations = SConscript('SConscript', exports='env', variant_dir=os.path.join('build', env.mode)) + package = SConscript('packaging/osx/SConscript', exports='env apps configurations retail_prefix') + install_package = env.Install('.', package) + Alias('package', install_package) + Default('install') diff --git a/defines.py b/defines.py index 03859bda..0215df6e 100644 --- a/defines.py +++ b/defines.py @@ -71,6 +71,10 @@ def version(self): return self._version + @property + def version_str(self): + return '%s.%s' % (self.version.major, self.version.minor) + @property def date(self): if self._date is None: @@ -125,6 +129,7 @@ def register_into(self, env): action=self.action, emitter=self.emitter, )}) + env.defines = self def generate_defines(self, target): """ diff --git a/packaging/osx/.gitignore b/packaging/osx/.gitignore index faa418a1..b6967424 100644 --- a/packaging/osx/.gitignore +++ b/packaging/osx/.gitignore @@ -1,2 +1,3 @@ -*.pkg -org.freelan.freelan +distribution.xml +root/ +resources/conclusion.html diff --git a/packaging/osx/Makefile b/packaging/osx/Makefile deleted file mode 100644 index f1d99a24..00000000 --- a/packaging/osx/Makefile +++ /dev/null @@ -1,23 +0,0 @@ -PRODUCT_NAME=freelan -IDENTIFIER=org.freelan.freelan -VERSION=2.0 -DAEMON_PACKAGE=${IDENTIFIER}.pkg -FINAL_PACKAGE=${PRODUCT_NAME}_${VERSION}.pkg - -default: package - -clean: - rm -rf ${IDENTIFIER} - rm -f ${DAEMON_PACKAGE} - rm -f ${FINAL_PACKAGE} - -package: clean - mkdir -p ${IDENTIFIER} - mkdir -p ${IDENTIFIER}/usr/local - mkdir -p ${IDENTIFIER}/usr/local/share/freelan - mkdir -p ${IDENTIFIER}/Library/LaunchDaemons - cp -r ../../install/* ${IDENTIFIER}/usr/local/ - cp org.freelan.freelan.plist ${IDENTIFIER}/Library/LaunchDaemons/ - cp uninstall.sh ${IDENTIFIER}/usr/local/share/freelan/ - pkgbuild --root ${IDENTIFIER} --identifier ${IDENTIFIER} --version ${VERSION} --ownership recommended --scripts scripts ${DAEMON_PACKAGE} - productbuild --distribution distribution.xml --resources resources --package-path . --version ${VERSION} ${FINAL_PACKAGE} diff --git a/packaging/osx/README.md b/packaging/osx/README.md index 665c5193..7797897c 100644 --- a/packaging/osx/README.md +++ b/packaging/osx/README.md @@ -1,3 +1,5 @@ # Mac OS X Installer -To build the Mac OS X package, just type "make". +To build the Mac OS X package, just type "scons -u .". + +You can also build it from the repository root by typing "scons package". diff --git a/packaging/osx/SConscript b/packaging/osx/SConscript new file mode 100644 index 00000000..f2b712a5 --- /dev/null +++ b/packaging/osx/SConscript @@ -0,0 +1,104 @@ +import os +import pkgbuild +import productbuild +import plist +import generate_script +import template + + +def relative(path): + return path.lstrip('/') + + +Import('env apps configurations retail_prefix') + +env = env.Clone() + +for module in [pkgbuild, productbuild, plist, generate_script, template]: + module.generate(env) + +root = env.Dir('root') +scripts = env.Dir('scripts') +options = { + 'identifier': 'org.freelan.freelan', + 'version': env.defines.version_str, + 'ownership': 'recommended', +} +resources = env.Dir('resources') +distribution_template = env.File('distribution.xml.in') +conclusion_template = resources.File('conclusion.html.in') + +bin_path = os.path.join(retail_prefix, 'bin') +etc_freelan_path = os.path.join(retail_prefix, 'etc/freelan') +share_freelan_path = os.path.join(retail_prefix, 'share/freelan') +uninstall_script = os.path.join(share_freelan_path, 'uninstall.sh') +launch_daemon_script = os.path.join( + '/Library/LaunchDaemons', + options['identifier'] + '.plist', +) + +uninstall_script_source = env.Value([ + '/bin/launchctl unload ' + launch_daemon_script, + 'rm -f ' + launch_daemon_script, + 'rm -f ' + os.path.join(bin_path, apps[0].name), + 'rm -rf ' + etc_freelan_path, +]) +launch_daemon_script_source = env.Value({ + 'Label': options['identifier'], + 'ProgramArguments': [ + os.path.join(bin_path, apps[0].name), + '-c', + os.path.join(etc_freelan_path, configurations[0].name), + '-f', + ], + 'RunAtLoad': True, + 'KeepAlive': True, +}) + +env.Install( + root.Dir(relative(bin_path)), + apps, +) +env.Install( + root.Dir(relative(etc_freelan_path)), + configurations, +) +env.GenerateScript( + root.File(relative(uninstall_script)), + uninstall_script_source, +) +env.Plist( + root.File(relative(launch_daemon_script)), + launch_daemon_script_source, +) + +package = env.PkgBuild( + target=options['identifier'] + '.pkg', + source=root, + PKGBUILD_OPTIONS=env.Value(options), + PKGBUILD_SCRIPTS=scripts, +) +distribution_file = env.Template( + source=distribution_template, + TEMPLATE_DICT=env.Value({'version': env.defines.version_str}), +) +conclusion_file = env.Template( + source=conclusion_template, + TEMPLATE_DICT=env.Value({ + 'configuration_file': os.path.join( + etc_freelan_path, + configurations[0].name, + ) + }), +) +final_package = env.ProductBuild( + target='freelan_{version}.pkg'.format(version=env.defines.version_str), + source=distribution_file, + PRODUCTBUILD_OPTIONS=env.Value({ + 'version': env.defines.version_str, + }), + PRODUCTBUILD_RESOURCES=resources, + PRODUCTBUILD_PACKAGE_PATH=[env.Dir('.')], +) + +Return('final_package') diff --git a/packaging/osx/distribution.xml b/packaging/osx/distribution.xml.in similarity index 84% rename from packaging/osx/distribution.xml rename to packaging/osx/distribution.xml.in index fbc67bd8..4a1a289c 100644 --- a/packaging/osx/distribution.xml +++ b/packaging/osx/distribution.xml.in @@ -1,6 +1,6 @@ - freelan 1.1 + FreeLAN {version} org.freelan @@ -18,7 +18,7 @@ - + diff --git a/packaging/osx/generate_script.py b/packaging/osx/generate_script.py new file mode 100644 index 00000000..752e6eeb --- /dev/null +++ b/packaging/osx/generate_script.py @@ -0,0 +1,36 @@ +"""A SCons builder for plist files""" + + +def generate_script_emitter(target, source, env): + env.Depends(target, env.Value(env['GENERATE_SCRIPT_TEMPLATE'])) + + return (target, source) + + +def generate_script_action(target, source, env): + template = env['GENERATE_SCRIPT_TEMPLATE'] + + for targ in target: + with open(targ.abspath, 'w') as targf: + targf.write( + template.format( + commands='\n'.join(source[0].value), + ), + ) + + +def generate(env): + env.Append(GENERATE_SCRIPT_TEMPLATE="""#!/bin/sh + +{commands} +""") + + import SCons.Builder + + generate_script_builder = SCons.Builder.Builder( + action=generate_script_action, + emitter=generate_script_emitter, + suffix='.sh', + ) + + env.Append(BUILDERS={'GenerateScript': generate_script_builder}) diff --git a/packaging/osx/org.freelan.freelan.plist b/packaging/osx/org.freelan.freelan.plist deleted file mode 100644 index f144a751..00000000 --- a/packaging/osx/org.freelan.freelan.plist +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Label - org.freelan.freelan - ProgramArguments - - /usr/local/bin/freelan - -c - /usr/local/etc/freelan/freelan.cfg - -f - - RunAtLoad - KeepAlive - - diff --git a/packaging/osx/pkgbuild.py b/packaging/osx/pkgbuild.py new file mode 100644 index 00000000..03a02429 --- /dev/null +++ b/packaging/osx/pkgbuild.py @@ -0,0 +1,72 @@ +"""A SCons builder for pkgbuild""" + +import SCons.Warnings +import SCons.Errors + + +def pkgbuild_emitter(target, source, env): + """The emitter""" + + env.Depends(target, env['PKGBUILD_OPTIONS']) + env.Depends(target, env['PKGBUILD_SCRIPTS']) + + return (target, source) + + +def pkgbuild_generator(target, source, env, for_signature): + """The generator""" + + options = env['PKGBUILD_OPTIONS'].value + options_str = ' '.join( + '--%s %s' % (key, value) + for key, value in options.iteritems() + ) + + if env['PKGBUILD_SCRIPTS']: + options_str = options_str + ' --scripts $PKGBUILD_SCRIPTS' + + return '{executable} {options_str} --root $SOURCE $TARGET'.format( + executable=env['PKGBUILD'], + options_str=options_str, + ) + + +class PkgBuildNotFound(SCons.Warnings.Warning): + pass + + +def detect(env): + try: + return env['PKGBUILD'] + except KeyError: + pass + + pkgbuild = env.WhereIs('pkgbuild') + + if pkgbuild: + return pkgbuild + + raise SCons.Errors.StopError( + PkgBuildNotFound, + "Unable to find pkgbuild" + ) + + +def generate(env): + env.Append(PKGBUILD=detect(env)) + env.Append(PKGBUILD_SCRIPTS=None) + env.Append(PKGBUILD_OPTIONS=env.Value({})) + + import SCons.Builder + + pkgbuild_builder = SCons.Builder.Builder( + generator=pkgbuild_generator, + emitter=pkgbuild_emitter, + suffix='.pkg', + ) + + env.Append(BUILDERS={'PkgBuild': pkgbuild_builder}) + + +def exists(env): + return env.Detect(env['PKGBUILD']) diff --git a/packaging/osx/plist.py b/packaging/osx/plist.py new file mode 100644 index 00000000..50f8819b --- /dev/null +++ b/packaging/osx/plist.py @@ -0,0 +1,19 @@ +"""A SCons builder for plist files""" + +import plistlib + + +def plist_action(target, source, env): + for targ in target: + plistlib.writePlist(source[0].value, targ.abspath) + + +def generate(env): + import SCons.Builder + + plist_builder = SCons.Builder.Builder( + action=plist_action, + suffix='.plist', + ) + + env.Append(BUILDERS={'Plist': plist_builder}) diff --git a/packaging/osx/productbuild.py b/packaging/osx/productbuild.py new file mode 100644 index 00000000..fda5b884 --- /dev/null +++ b/packaging/osx/productbuild.py @@ -0,0 +1,136 @@ +"""A SCons builder for productbuild""" + +from xml.dom import minidom + +import SCons.Warnings +import SCons.Errors + + +def get_nodes(element, path): + if not hasattr(element, 'nodeName'): + return [] + + separator = '/' + + if separator in path: + tag, subpath = path.split(separator, 1) + else: + tag, subpath = path, None + + if tag == '': + tag = '#document' + + if element.nodeName != tag: + return [] + + if subpath: + result = [] + + for node in element.childNodes: + result.extend(get_nodes(node, subpath)) + + return result + + return [element] + + +def productbuild_scanner(node, env, paths): + document = minidom.parseString(node.get_contents()) + xnodes = get_nodes(document, '/installer-gui-script/pkg-ref') + packages = [xnode.childNodes[0].nodeValue for xnode in xnodes] + + result = [] + + for package in packages: + for path in paths: + package_file = env.Dir(path).File(package) + result.append(package_file) + break + + return result + + +def productbuild_emitter(target, source, env): + """The emitter""" + + env.Depends(target, env['PRODUCTBUILD_OPTIONS']) + env.Depends(target, env['PRODUCTBUILD_RESOURCES']) + + return (target, source) + + +def productbuild_generator(target, source, env, for_signature): + """The generator""" + + options = env['PRODUCTBUILD_OPTIONS'].value + options_str = ' '.join( + '--%s %s' % (key, value) + for key, value in options.iteritems() + ) + + if env['PRODUCTBUILD_RESOURCES']: + options_str = options_str + ' --resources $PRODUCTBUILD_RESOURCES' + + package_path = env['PRODUCTBUILD_PACKAGE_PATH'] + + if package_path: + options_str = options_str + ' ' + ' '.join( + '--package-path %s' % path + for path in package_path + ) + + return '{executable} {options_str} --distribution $SOURCE $TARGET'.format( + executable=env['PRODUCTBUILD'], + options_str=options_str, + ) + + +class ProductBuildNotFound(SCons.Warnings.Warning): + pass + + +def detect(env): + try: + return env['PRODUCTBUILD'] + except KeyError: + pass + + productbuild = env.WhereIs('productbuild') + + if productbuild: + return productbuild + + raise SCons.Errors.StopError( + ProductBuildNotFound, + "Unable to find productbuild" + ) + + +def generate(env): + env.Append(PRODUCTBUILD=detect(env)) + env.Append(PRODUCTBUILD_PACKAGE_PATH=[]) + env.Append(PRODUCTBUILD_RESOURCES=None) + env.Append(PRODUCTBUILD_OPTIONS=env.Value({})) + + import SCons.Scanner + + env.Append(SCANNERS=SCons.Scanner.Scanner( + function=productbuild_scanner, + skeys=['.xml'], + path_function=SCons.Scanner.FindPathDirs('PRODUCTBUILD_PACKAGE_PATH'), + )) + + import SCons.Builder + + productbuild_builder = SCons.Builder.Builder( + generator=productbuild_generator, + emitter=productbuild_emitter, + suffix='.pkg', + src_suffix='.xml', + ) + + env.Append(BUILDERS={'ProductBuild': productbuild_builder}) + + +def exists(env): + return env.Detect(env['PRODUCTBUILD']) diff --git a/packaging/osx/resources/conclusion.html b/packaging/osx/resources/conclusion.html deleted file mode 100644 index 9692ff3a..00000000 --- a/packaging/osx/resources/conclusion.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - -

Installation complete !

-

You may go to www.freelan.org for configuration instructions.

- - diff --git a/packaging/osx/resources/conclusion.html.in b/packaging/osx/resources/conclusion.html.in new file mode 100644 index 00000000..8a998011 --- /dev/null +++ b/packaging/osx/resources/conclusion.html.in @@ -0,0 +1,9 @@ + + + + +

Installation complete !

+

Go and edit the configuration file at {configuration_file} to get started !

+

You may also check the official website for detailed configuration instructions.

+ + diff --git a/packaging/osx/template.py b/packaging/osx/template.py new file mode 100644 index 00000000..563b0fd6 --- /dev/null +++ b/packaging/osx/template.py @@ -0,0 +1,32 @@ +"""A SCons builder for template files""" + + +def template_emitter(target, source, env): + env.Depends(target, env['TEMPLATE_DICT']) + + return (target, source) + + +def template_action(target, source, env): + _dict = env['TEMPLATE_DICT'].value + + template = source[0].get_contents() + + with open(target[0].abspath, 'w') as targf: + targf.write( + template.format(**_dict) + ) + + +def generate(env): + env.Append(TEMPLATE_DICT=env.Value({})) + + import SCons.Builder + + template_builder = SCons.Builder.Builder( + action=template_action, + emitter=template_emitter, + src_suffix='.in', + ) + + env.Append(BUILDERS={'Template': template_builder}) diff --git a/packaging/osx/uninstall.sh b/packaging/osx/uninstall.sh deleted file mode 100755 index 9d76a85e..00000000 --- a/packaging/osx/uninstall.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -/bin/launchctl unload /Library/LaunchDaemons/org.freelan.freelan.plist -rm -f /Library/LaunchDaemons/org.freelan.freelan.plist -rm -f /usr/local/sbin/freelan -rm -f /usr/local/etc/freelan/freelan.cfg