diff options
author | Andoni Morales Alastruey <ylatuya@gmail.com> | 2012-06-25 11:57:14 +0200 |
---|---|---|
committer | Andoni Morales Alastruey <ylatuya@gmail.com> | 2012-06-25 12:27:34 +0200 |
commit | 344fbc9721a2e721e831a3c5895e911977ba7ed1 (patch) | |
tree | 9938af87cc254a61804e32bd104b18c00d47e568 | |
parent | bb092276471acd4cb1b08a26749c62df456b2795 (diff) |
Add initial support for application packages
-rw-r--r-- | cerbero/packages/osx/bundles.py | 181 | ||||
-rw-r--r-- | cerbero/packages/osx/packager.py | 96 | ||||
-rw-r--r-- | cerbero/packages/package.py | 106 | ||||
-rw-r--r-- | cerbero/packages/packagesstore.py | 5 | ||||
-rw-r--r-- | test/test_build_common.py | 1 | ||||
-rw-r--r-- | test/test_cerbero_packages_package.py | 49 | ||||
-rw-r--r-- | test/test_packages_common.py | 19 |
7 files changed, 447 insertions, 10 deletions
diff --git a/cerbero/packages/osx/bundles.py b/cerbero/packages/osx/bundles.py new file mode 100644 index 00000000..32165fd0 --- /dev/null +++ b/cerbero/packages/osx/bundles.py @@ -0,0 +1,181 @@ +# cerbero - a multi-platform build system for Open Source software +# Copyright (C) 2012 Andoni Morales Alastruey <ylatuya@gmail.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Library General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Library General Public License for more details. +# +# You should have received a copy of the GNU Library General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +import os +import tempfile +import shutil +from stat import S_IXUSR, S_IXGRP, S_IXOTH + +from cerbero.packages import PackagerBase +from cerbero.packages.osx.packagemaker import PackageMaker +from cerbero.packages.osx.info_plist import FrameworkPlist, ApplicationPlist +from cerbero.utils import shell + + +class BundlePackagerBase(PackagerBase): + ''' + Creates a package with the basic structure of a bundle, to be included + in a MetaPackage. + ''' + + name = '' + title = '' + + def __init__(self, config, package, store): + PackagerBase.__init__(self, config, package, store) + + def pack(self, output_dir): + output_dir = os.path.realpath(output_dir) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + self.install_dir = self.package.get_install_dir() + + path = self._create_package(output_dir, self.package.get_install_dir(), + self.package.version) + return [path, None] + + + def _create_package(self, output_dir, install_dir, version): + output_file = os.path.join(output_dir, '%s-%s-%s.pkg' % + (self.name, self.package.version, + self.config.target_arch)) + root = self.create_bundle() + packagemaker = PackageMaker() + packagemaker.create_package(root, self.package_name, + self.package.version, self.title, output_file, + install_dir, target=None) + return output_file + + def get_install_dir(self): + ''' + Get the installation directory + ''' + raise NotImplemented('Subclasses should implement get_install_dir') + + def create_bundle(self): + ''' + Creates the bundle structure + ''' + raise NotImplemented('Subclasses should implement create_bundle') + + +class FrameworkBundlePackager(BundlePackagerBase): + ''' + Creates a package with the basic structure of a framework bundle, + adding links for Headears, Libraries, Commands, and Current Versions, + and the Framework info. + ''' + + name = 'osx-framework' + title = 'Framework Bundle' + + def __init__(self, config, package, store): + BundlePackagerBase.__init__(self, config, package, store) + + def get_install_dir(self): + return os.path.join(self.install_dir, 'Versions', + self.package.version, self.config.target_arch) + + def create_bundle(self): + ''' + Creates the bundle structure + + Commands -> Versions/Current/bin + Headers -> Versions/Current/Headers + Librarires -> Versions/Current/lib + Home -> Versions/Current + Resources -> Versions/Current/Resources + Versions/Current -> Version/$VERSION/$ARCH + ''' + tmp = tempfile.mkdtemp() + + vdir = 'Versions/%s/%s' % (self.package.version, + self.config.target_arch) + rdir = '%s/Resources/' % vdir + shell.call ('mkdir -p %s' % rdir, tmp) + links = {'Versions/Current': '../%s' % vdir, + 'Resources': 'Versions/Current/Resources', + 'Commands': 'Versions/Current/bin', + 'Headers': 'Versions/Current/Headers', + 'Libraries': 'Versions/Current/lib'} + framework_plist = FrameworkPlist(self.package.name, + self.package.org, self.package.version, self.package.shortdesc) + framework_plist.save(os.path.join(tmp, rdir, 'Info.plist')) + for dest, src in links.iteritems(): + shell.call ('ln -s %s %s' % (src, dest), tmp) + if self.package.osx_framework_library is not None: + name, link = self.package.osx_framework_library + link = os.path.join('Versions', 'Current', link) + shell.call ('ln -s %s %s' % (link, name), tmp) + return tmp + + +class ApplicationBundlePackager(BundlePackagerBase): + ''' + Creates a package with the basic structure of an Application bundle. + ''' + + def __init__(self, config, package, store): + BundlePackagerBase.__init__(self, config, package, store) + + def get_install_dir(self): + return self.install_dir + + def create_bundle(self, tmp=None): + ''' + Creates the Application bundle structure + + Contents/MacOS/MainExectuable -> Contents/Home/bin/main-executable + Contents/Info.plist + ''' + tmp = tmp or tempfile.mkdtemp() + + contents = os.path.join(tmp, 'Contents') + macos = os.path.join(contents, 'MacOS') + resources = os.path.join(contents, 'Resources') + for p in [contents, macos, resources]: + if not os.path.exists(p): + os.makedirs(p) + + # Create Contents/Info.plist + framework_plist = ApplicationPlist(self.package.name, + self.package.org, self.package.version, self.package.shortdesc, + os.path.basename(self.package.resources_icon_icns)) + framework_plist.save(os.path.join(contents, 'Info.plist')) + + # Copy app icon to Resources + shutil.copy(self.package.resources_icon_icns, resources) + + # Link or create a wrapper for the executables in Contents/MacOS + for name, path, use_wrapper, wrapper in self.package.get_commands(): + filename = os.path.join(macos, name) + if use_wrapper: + wrapper = self.package.get_wrapper(path, wrapper) + if not wrapper: + continue + with open(filename, 'w') as f: + f.write(wrapper) + shell.call('chmod +x %s' % filename) + else: + # FIXME: We need to copy the binary instead of linking, because + # beeing a different path, @executable_path will be different + # and it we will need to set a different relative path with + # install_name_tool + shutil.copy(os.path.join(contents, 'Home', path), macos) + return tmp diff --git a/cerbero/packages/osx/packager.py b/cerbero/packages/osx/packager.py index 2ccdea20..85f9ebfe 100644 --- a/cerbero/packages/osx/packager.py +++ b/cerbero/packages/osx/packager.py @@ -23,10 +23,12 @@ import shutil from cerbero.ide.pkgconfig import PkgConfig from cerbero.errors import EmptyPackageError from cerbero.packages import PackagerBase, PackageType -from cerbero.packages.package import Package, PackageBase +from cerbero.packages.package import Package, MetaPackage, App, PackageBase from cerbero.packages.osx.pmdoc import PMDoc -from cerbero.packages.osx.bundles import FrameworkBundlePackager +from cerbero.packages.osx.bundles import FrameworkBundlePackager,\ + ApplicationBundlePackager from cerbero.packages.osx.packagemaker import PackageMaker +from cerbero.tools.osxrelocator import OSXRelocator from cerbero.utils import shell, _ from cerbero.utils import messages as m @@ -302,6 +304,94 @@ class PMDocPackage(PackagerBase): shell.call(cmd) +class ApplicationPackage(PackagerBase): + ''' + Creates an osx package from a L{cerbero.packages.package.Package} + + @ivar package: package used to create the osx package + @type package: L{cerbero.packages.package.Package} + ''' + + def __init__(self, config, package, store): + PackagerBase.__init__(self, config, package, store) + + def pack(self, output_dir, devel=False, force=False, keep_temp=False): + PackagerBase.pack(self, output_dir, devel, force, keep_temp) + + self.tmp = tempfile.mkdtemp() + self.tmp = os.path.join(self.tmp, '%s.app' % self.package.app_name) + + # copy files to the bundle. it needs to be done first because the app + # bundle will try to create links for the main executable + self._create_bundle() + self._create_app_bundle() + self._strip_binaries() + self._relocate_binaries() + dmg = self._create_dmg() + + return [dmg, None] + + def _create_bundle(self): + ''' + Moves all the files that are going to be packaged to the bundle's + temporary directory + ''' + out_dir = os.path.join(self.tmp, 'Contents', 'Home') + os.makedirs(out_dir) + for f in self.package.files_list(): + in_path = os.path.join(self.config.prefix, f) + if not os.path.exists(in_path): + m.warning("File %s is missing and won't be added to the " + "package" % in_path) + continue + out_path = os.path.join(out_dir, f) + odir = os.path.split(out_path)[0] + if not os.path.exists(odir): + os.makedirs(odir) + shutil.copy(in_path, out_path) + + def _create_app_bundle(self): + ''' Creates the OS X Application bundle in temporary directory ''' + packager = ApplicationBundlePackager(self.config, self.package, + self.store) + return packager.create_bundle(self.tmp) + + def _strip_binaries(self): + pass + + def _relocate_binaries(self): + prefix = self.config.prefix + if prefix[-1] == '/': + prefix = prefix[:-1] + for path in ['bin']: + relocator = OSXRelocator( + os.path.join(self.tmp, 'Contents', 'Home', path), + self.config.prefix, '@executable_path/../', True) + relocator.relocate() + for path in ['lib', 'libexec']: + relocator = OSXRelocator( + os.path.join(self.tmp, 'Contents', 'Home', path), + self.config.prefix, '@loader_path/../', True) + relocator.relocate() + relocator = OSXRelocator( + os.path.join(self.tmp, 'Contents', 'MacOS', path), + self.config.prefix, '@executable_path/../Home/', False) + relocator.relocate() + + def _create_dmg(self): + #applications_link = os.path.join(self.tmp, 'Applications') + #shell.call('ln -s /Applications %s' % applications_link) + # Create link to /Applications + dmg_file = os.path.join(self.output_dir, '%s-%s-%s.dmg' % ( + self.package.app_name, self.package.version, self.config.target_arch)) + # Create Disk Image + cmd = 'hdiutil create %s -volname %s -ov -srcfolder %s' % \ + (dmg_file, self.package.app_name, self.tmp) + shell.call(cmd) + return dmg_file + + + class Packager(object): def __new__(klass, config, package, store): @@ -309,6 +399,8 @@ class Packager(object): return OSXPackage(config, package, store) elif isinstance(package, MetaPackage): return PMDocPackage(config, package, store) + elif isinstance(package, App): + return ApplicationPackage(config, package, store) def register(): diff --git a/cerbero/packages/package.py b/cerbero/packages/package.py index 17f8990f..fc6b7617 100644 --- a/cerbero/packages/package.py +++ b/cerbero/packages/package.py @@ -55,8 +55,10 @@ class PackageBase(object): @type resources_license: str @cvar resources_license_rtf: filename of .rtf license file @type resources_license_rtf: str - @cvar resources_icon: filename of the icon image + @cvar resources_icon: filename of the .ico icon @type resources_icon: str + @cvar resources_icon_icns: filename of the .icsn icon + @type resources_icon_icns: str @cvar resources_backgound = filename of the background image @type resources_backgound = str ''' @@ -75,6 +77,7 @@ class PackageBase(object): resources_license = 'license.txt' resources_license_rtf = 'license.txt' resources_icon = 'icon.ico' + resources_icon_icns = 'icon.icns' resources_background = 'background.png' def __init__(self, config, store): @@ -292,8 +295,6 @@ class MetaPackage(PackageBase): @type packages: list @cvar platform_packages: list of platform packages @type platform_packages: dict - @cvar icon: filename of the package icon - @type icon: str @cvar root_env_var: name of the environment variable with the prefix @type root_env_var: str @cvar sdk_version: SDK version. This version will be used for the SDK @@ -401,3 +402,102 @@ class InstallerPackage(MetaPackage): def __init__(self, config, store): MetaPackage.__init__(self, config, store) + + +class App(PackageBase): + ''' + Create packages for applications. + An App package will not include development files and binaries could + be stripped when required. The App packager will not create a development + version. + On linux it will work in the same way as a MetaPackage, creating a package + with the application's recipe files and adding packages dependencies to be + managed by the distribution's package manager. + On OS X and Windows, the dependencies could be embeded in the installer + itself, creating an Application bundle on OS X and main menu shortcuts on + Windows, relocating the binaries properly. + + @cvar app_recipe: Name used for the application + @type app_recipe: str + @cvar app_recipe: recipe that builds the application project + @type app_recipe: str + @cvar deps: list of packages dependencies + @type deps: list + @cvar embed_deps: include dependencies in the final package + @type embed_deps: boolean + @cvar commands: a list of with the application commands. The first will be + used for the main executable + @type command: list + @cvar wrapper: suffix filename for the main executable wrapper + @type wrapper: str + ''' + + app_name = None + app_recipe = None + embed_deps = True + deps = [] + commands = [] # list of tuples ('CommandName', path/to/binary') + wrapper = 'app_wrapper.tpl' + + def __init__(self, config, store, cookbook): + PackageBase.__init__(self, config, store) + self.cookbook = cookbook + self._app_recipe = self.cookbook.get_recipe(self.app_recipe) + + def recipes_dependencies(self): + deps = [] + for dep in self.deps: + for package in self.store.get_package_deps(dep, True): + deps.extend(package.recipes_dependencies()) + return list(set(deps)) + + def files_list(self): + # for each package, call the function that list files + files = [] + if self.embed_deps and self.config.target_platform != Platform.LINUX: + packages_deps = [self.store.get_package(x) for x in self.deps] + for package in packages_deps: + packages_deps.extend(self.store.get_package_deps(package)) + packages_deps = list(set(packages_deps)) + for package in packages_deps: + files.extend(package.files_list()) + files.extend(self._app_recipe.files_list()) + files.sort() + return files + + def devel_files_list(self): + return [] + + def all_files_list(self): + return self.files_list() + + def get_wix_upgrade_code(self): + m = self.package_mode + p = self.config.target_arch + return self.wix_upgrade_code[m][p] + + def get_commands(self): + return self.commands + + def get_wrapper(self, cmd, wrapper=None): + if self.config.target_platform == Platform.WINDOWS: + platform = 'win' + else: + platform = 'unix' + + if wrapper is not None: + wrapper_file = self.relative_path('%s_%s' % (platform, wrapper)) + else: + wrapper_file = os.path.join(self.config.data_dir, 'templates', + '%s_%s' % (self.wrapper, platform)) + + if not os.path.exists(wrapper_file): + return None + + with open(wrapper_file, 'r') as f: + content = f.read() + content = content % {'prefix': self.config.prefix, + 'py_prefix': self.config.py_prefix, + 'cmd': self.config.prefix} + + return content diff --git a/cerbero/packages/packagesstore.py b/cerbero/packages/packagesstore.py index d7afc063..5279b5cd 100644 --- a/cerbero/packages/packagesstore.py +++ b/cerbero/packages/packagesstore.py @@ -196,8 +196,11 @@ class PackagesStore (object): p = d['SDKPackage'](self._config, self) elif 'InstallerPackage' in d: p = d['InstallerPackage'](self._config, self) + elif 'App' in d: + p = d['App'](self._config, self, self.cookbook) else: - raise Exception('Package or MetaPackage class found') + raise Exception('Package, SDKPackage, InstallerPackage or App ' + 'class not found') p.prepare() # reload files from package now that we called prepare that # may have changed it diff --git a/test/test_build_common.py b/test/test_build_common.py index e2cfa608..7a0e08a6 100644 --- a/test/test_build_common.py +++ b/test/test_build_common.py @@ -92,6 +92,7 @@ def add_files(tmp): 'README2 ' 'README3 ' 'README4 ' + 'README5 ' 'bin/gst-launch.exe ' 'bin/gst-launch ' 'bin/windows.exe ' diff --git a/test/test_cerbero_packages_package.py b/test/test_cerbero_packages_package.py index dc4e3083..7344a4a2 100644 --- a/test/test_cerbero_packages_package.py +++ b/test/test_cerbero_packages_package.py @@ -22,7 +22,7 @@ import tempfile from cerbero.config import Platform, Distro, DistroVersion from cerbero.packages import PackageType -from test.test_packages_common import Package1, Package4, MetaPackage +from test.test_packages_common import Package1, Package4, MetaPackage, App from test.test_build_common import create_cookbook, add_files from test.test_packages_common import create_store from test.test_common import DummyConfig @@ -111,8 +111,6 @@ class PackageTest(unittest.TestCase): self.assertEquals(package.get_sys_deps(), ['python27']) - - class TestMetaPackages(unittest.TestCase): def setUp(self): @@ -158,3 +156,48 @@ class TestMetaPackages(unittest.TestCase): def testAllFilesList(self): self._compareList('all_files_list') + + +class AppPackageTest(unittest.TestCase): + + def setUp(self): + self.tmp = tempfile.mkdtemp() + config = Config(self.tmp, Platform.LINUX) + self.store = create_store(config) + self.cookbook = create_cookbook(config) + self.app = App(config, self.store, self.cookbook) + + def tearDown(self): + shutil.rmtree(self.tmp) + + def testListFilesWithEmbededDepsOnLinux(self): + self.app.embed_deps = True + expected = self.app._app_recipe.files_list() + result = self.app.files_list() + self.assertEquals(expected, result) + + def testListFilesWithEmbededDeps(self): + self.app.embed_deps = True + self.app.config.target_platform = Platform.WINDOWS + files = [] + packages_deps = [self.store.get_package(x) for x in self.app.deps] + for dep in self.app.deps: + packages_deps.extend(self.store.get_package_deps(dep)) + packages_deps = list(set(packages_deps)) + for package in packages_deps: + files.extend(package.files_list()) + files.extend(self.app._app_recipe.files_list()) + files = sorted(set(files)) + self.assertEquals(files, self.app.files_list()) + + def testListFilesWithoutEmbededDeps(self): + self.app.embed_deps = False + expected = self.app._app_recipe.files_list() + result = self.app.files_list() + self.assertEquals(expected, result) + + def testDevelFilesList(self): + self.assertEquals(self.app.devel_files_list(), []) + + def testAllFilesList(self): + self.assertEquals(self.app.files_list(), self.app.all_files_list()) diff --git a/test/test_packages_common.py b/test/test_packages_common.py index 33ff2a70..adc9d385 100644 --- a/test/test_packages_common.py +++ b/test/test_packages_common.py @@ -96,6 +96,23 @@ class MetaPackage(package.MetaPackage): icon = "gstreamer.ico" +class App(package.App): + + name = "gstreamer-app" + shortdesc = "GStreamer sample app" + longdesc = "GStreamer sample app" + title = "GStreamer sample app" + url = "http://www.gstreamer.net" + version = '1.0' + uuid = '3ffe67b2-4565-411f-8287-e8faa892f853' + vendor = "GStreamer Project" + org = 'net.gstreamer' + app_recipe = 'recipe3' + deps = ['gstreamer-test1'] + icon = "share/images/gstreamer.png" + embed_deps = True + + class DummyConfig(object): pass @@ -104,7 +121,7 @@ def create_store(config): cookbook = create_cookbook(config) store = PackagesStore(config, False) - for klass in [Package1, Package2, Package3, Package4]: + for klass in [Package1, Package2, Package3, Package4, App]: package = klass(config, store, cookbook) store.add_package(package) for klass in [MetaPackage]: |