def freeze_mac(): if not exists(path('target/Icon.icns')): _generate_iconset() run(['iconutil', '-c', 'icns', path('target/Icon.iconset')], check=True) pyinstaller_args = ['--windowed', '--icon', path('target/Icon.icns')] bundle_identifier = SETTINGS.get('mac_bundle_identifier', '') if bundle_identifier: pyinstaller_args.extend(['--osx-bundle-identifier', bundle_identifier]) run_pyinstaller(extra_args=pyinstaller_args) _remove_unwanted_pyinstaller_files() _fix_sparkle_delta_updates() generate_resources( dest_dir=path('${freeze_dir}'), dest_dir_for_base=path('${freeze_dir}/Contents/Resources'))
def freeze_mac(extra_pyinstaller_args=None, debug=False): if extra_pyinstaller_args is None: extra_pyinstaller_args = [] if not exists(path('target/Icon.icns')): _generate_iconset() run(['iconutil', '-c', 'icns', path('target/Icon.iconset')], check=True) pyinstaller_args = ['--windowed', '--icon', path('target/Icon.icns')] bundle_identifier = SETTINGS['mac_bundle_identifier'] if bundle_identifier: pyinstaller_args.extend(['--osx-bundle-identifier', bundle_identifier]) run_pyinstaller(pyinstaller_args + extra_pyinstaller_args, debug) _remove_unwanted_pyinstaller_files() _fix_sparkle_delta_updates() generate_resources()
def release(): """ Bump version and run clean,freeze,...,upload """ require_existing_project() version = SETTINGS['version'] next_version = _get_next_version(version) release_version = prompt_for_value('Release version', default=next_version) activate_profile('release') SETTINGS['version'] = release_version log_level = _LOG.level if log_level == logging.NOTSET: _LOG.setLevel(logging.WARNING) try: clean() freeze() installer() sign_installer() repo() finally: _LOG.setLevel(log_level) upload() base_json = 'src/build/settings/base.json' update_json(path(base_json), {'version': release_version}) _LOG.info('Also, %s was updated with the new version.', base_json)
def test_freeze_installer(self): freeze() if is_mac(): executable = path('${freeze_dir}/Contents/MacOS/${app_name}') elif is_windows(): executable = path('${freeze_dir}/${app_name}.exe') else: executable = path('${freeze_dir}/${app_name}') self.assertTrue(exists(executable), executable + ' does not exist') installer() self.assertTrue(exists(path('target/${installer}'))) if is_linux(): applications_dir = path('target/installer/usr/share/applications') self.assertEqual(['MyApp.desktop'], listdir(applications_dir)) with open(join(applications_dir, 'MyApp.desktop')) as f: self.assertIn('MyApp', f.read())
def test(): """ Execute your automated tests """ sys.path.append(path('src/main/python')) suite = TestSuite() test_dirs = SETTINGS['test_dirs'] for test_dir in map(path, test_dirs): sys.path.append(test_dir) try: dir_names = listdir(test_dir) except FileNotFoundError: continue for dir_name in dir_names: dir_path = join(test_dir, dir_name) if isfile(join(dir_path, '__init__.py')): suite.addTest(defaultTestLoader.discover( dir_name, top_level_dir=test_dir )) has_tests = bool(list(suite)) if has_tests: TextTestRunner().run(suite) else: print( 'No tests found. You can add them to:\n * '+ '\n * '.join(test_dirs) )
def run(): """ Run your app from source """ require_existing_project() if not _has_module('PyQt5') and not _has_module('PySide2'): raise FbsError("Couldn't find PyQt5 or PySide2. Maybe you need to:\n" " pip install PyQt5==5.9.2 or\n" " pip install PySide2==5.12.2") env = dict(os.environ) pythonpath = path('src/main/python') old_pythonpath = env.get('PYTHONPATH', '') if old_pythonpath: pythonpath += os.pathsep + old_pythonpath env['PYTHONPATH'] = pythonpath subprocess.run([sys.executable, path(SETTINGS['main_module'])], env=env)
def _add_missing_dlls(): freeze_dir = path('${freeze_dir}') for dll_name in ('msvcr100.dll', 'msvcr110.dll', 'msvcp110.dll', 'vcruntime140.dll', 'msvcp140.dll', 'concrt140.dll', 'vccorlib140.dll', 'api-ms-win-crt-multibyte-l1-1-0.dll'): if not exists(join(freeze_dir, dll_name)): copy(join(r'c:\Windows\System32', dll_name), freeze_dir)
def release(version=None): """ Bump version and run clean,freeze,...,upload """ require_existing_project() if version is None: curr_version = SETTINGS['version'] next_version = _get_next_version(curr_version) release_version = prompt_for_value('Release version', default=next_version) elif version == 'current': release_version = SETTINGS['version'] else: release_version = version activate_profile('release') SETTINGS['version'] = release_version log_level = _LOG.level if log_level == logging.NOTSET: _LOG.setLevel(logging.WARNING) try: clean() freeze() if is_windows() and _has_windows_codesigning_certificate(): sign() installer() if (is_windows() and _has_windows_codesigning_certificate()) or \ is_arch_linux() or is_fedora(): sign_installer() repo() finally: _LOG.setLevel(log_level) upload() base_json = 'src/build/settings/base.json' update_json(path(base_json), {'version': release_version}) _LOG.info('Also, %s was updated with the new version.', base_json)
def require_installer(): installer = path('target/${installer}') if not exists(installer): raise FbsError( 'Installer does not exist. Maybe you need to run:\n' ' fbs installer' )
def require_existing_project(): if not exists(path('src')): raise FbsError( "Could not find the src/ directory. Are you in the right folder?\n" "If yes, did you already run\n" " fbs startproject ?" )
def run_fpm(output_type): dest = path('target/${installer}') if exists(dest): remove(dest) # Lower-case the name to avoid the following fpm warning: # > Debian tools (dpkg/apt) don't do well with packages that use capital # > letters in the name. In some cases it will automatically downcase # > them, in others it will not. It is confusing. Best to not use any # > capital letters at all. name = SETTINGS['app_name'].lower() args = [ 'fpm', '-s', 'dir', # We set the log level to error because fpm prints the following warning # even if we don't have anything in /etc: # > Debian packaging tools generally labels all files in /etc as config # > files, as mandated by policy, so fpm defaults to this behavior for # > deb packages. You can disable this default behavior with # > --deb-no-default-config-files flag '--log', 'error', '-C', path('target/installer'), '-n', name, '-v', SETTINGS['version'], '--vendor', SETTINGS['author'], '-t', output_type, '-p', dest ] if SETTINGS['description']: args.extend(['--description', SETTINGS['description']]) if SETTINGS['author_email']: args.extend([ '-m', '%s <%s>' % (SETTINGS['author'], SETTINGS['author_email']) ]) if SETTINGS['url']: args.extend(['--url', SETTINGS['url']]) for dependency in SETTINGS['depends']: args.extend(['-d', dependency]) if is_arch_linux(): for opt_dependency in SETTINGS['depends_opt']: args.extend(['--pacman-optional-depends', opt_dependency]) try: run(args, check=True, stdout=DEVNULL) except FileNotFoundError: raise FileNotFoundError( "fbs could not find executable 'fpm'. Please install fpm using the " "instructions at " "https://fpm.readthedocs.io/en/latest/installing.html." ) from None
def create_installer_windows(): setup_nsi = join(dirname(__file__), 'Setup.nsi') copy_with_filtering(setup_nsi, path('target/NSIS'), replacements={ 'app_name': SETTINGS['app_name'], 'author': SETTINGS['author'] }, files_to_filter=[setup_nsi], placeholder='%%{%s}') try: run(['makensis', 'Setup.nsi'], cwd=path('target/NSIS'), check=True) except FileNotFoundError: raise FileNotFoundError( "fbs could not find executable 'makensis'. Please install NSIS and " "add its installation directory to your PATH environment variable." ) from None
def _generate_icons(): dest_root = path('target/installer/usr/share/icons/hicolor') makedirs(dest_root) icons_fname = '%s.png' % SETTINGS['app_name'] for size, icon_path in get_icons(): icon_dest = join(dest_root, '%dx%d' % (size, size), 'apps', icons_fname) makedirs(dirname(icon_dest)) copy(icon_path, icon_dest)
def test_generate_resources(self): self.init_fbs('Mac') _generate_resources() info_plist = path('${freeze_dir}/Contents/Info.plist') self.assertTrue(exists(info_plist)) with open(info_plist) as f: self.assertIn('MyApp', f.read(), "Did not replace '${app_name}' by 'MyApp'")
def init_licensing(): """ Generate public/private keys for licensing """ require_existing_project() try: import rsa except ImportError: _LOG.error('Please install Python library `rsa`. Eg. via:\n' ' pip install rsa') return nbits = _prompt_for_nbits() print('') pubkey, privkey = rsa.newkeys(nbits) pubkey_args = {'n': pubkey.n, 'e': pubkey.e} privkey_args = { attr: getattr(privkey, attr) for attr in ('n', 'e', 'd', 'p', 'q') } update_json(path(SECRET_JSON), { 'licensing_privkey': privkey_args, 'licensing_pubkey': pubkey_args }) try: with open(path(BASE_JSON)) as f: user_base_settings = json.load(f) except FileNotFoundError: user_base_settings = {} public_settings = user_base_settings.get('public_settings', []) if 'licensing_pubkey' not in public_settings: public_settings.append('licensing_pubkey') update_json(path(BASE_JSON), {'public_settings': public_settings}) updated_base_json = True else: updated_base_json = False message = 'Saved a public/private key pair for licensing to:\n %s.\n' \ % SECRET_JSON if updated_base_json: message += 'Also added "licensing_pubkey" to "public_settings" in' \ '\n %s.\n' \ '(This lets your app read the public key when it runs.)\n' \ % BASE_JSON message += '\nFor details on how to implement licensing for your ' \ 'application, see:\n '\ ' https://build-system.fman.io/manual#licensing.' _LOG.info(message)
def generate_resources(): freeze_dir = path('${freeze_dir}') if is_mac(): resources_dest_dir = join(freeze_dir, 'Contents', 'Resources') else: resources_dest_dir = freeze_dir kwargs = {'files_to_filter': SETTINGS['resources_to_filter']} resource_dirs = ( path('src/main/resources/base'), path('src/main/resources/' + platform.name().lower()) ) for dir_ in resource_dirs: if exists(dir_): copy_with_filtering(dir_, resources_dest_dir, **kwargs) frozen_resources_dir = dir_ + '-frozen' if exists(frozen_resources_dir): copy_with_filtering(frozen_resources_dir, freeze_dir, **kwargs)
def freeze_mac(debug=False): if not exists(path('target/Icon.icns')): _generate_iconset() run(['iconutil', '-c', 'icns', path('target/Icon.iconset')], check=True) args = [] if not (debug or SETTINGS['show_console_window']): args.append('--windowed') args.extend(['--icon', path('target/Icon.icns')]) bundle_identifier = SETTINGS['mac_bundle_identifier'] if bundle_identifier: args.extend(['--osx-bundle-identifier', bundle_identifier]) run_pyinstaller(args, debug) _remove_unwanted_pyinstaller_files() _fix_sparkle_delta_updates() _generate_resources()
def create_repo_ubuntu(): dest_dir = path('target/repo') tmp_dir = path('target/repo-tmp') if exists(dest_dir): rmtree(dest_dir) if exists(tmp_dir): rmtree(tmp_dir) makedirs(tmp_dir) distr_file = 'src/repo/ubuntu/distributions' distr_path = path(distr_file) if not exists(distr_path): distr_path = default_path(distr_file) copy_with_filtering(distr_path, tmp_dir, files_to_filter=[distr_path]) preset_gpg_passphrase() check_call([ 'reprepro', '-b', dest_dir, '--confdir', tmp_dir, 'includedeb', 'stable', path('target/${installer}') ], stdout=DEVNULL)
def sign_installer_arch(): installer = path('target/${installer}') # Prevent GPG from prompting us for the passphrase when signing: preset_gpg_passphrase() check_call([ 'gpg', '--batch', '--yes', '-u', SETTINGS['gpg_key'], '--output', installer + '.sig', '--detach-sig', installer ], stdout=DEVNULL)
def run_pyinstaller(extra_args=None, debug=False): if extra_args is None: extra_args = [] app_name = SETTINGS['app_name'] log_level = 'DEBUG' if debug else 'WARN' cmdline = [ 'pyinstaller', '--name', app_name, '--noupx', '--log-level', log_level ] + extra_args + [ '--distpath', path('target'), '--specpath', path('target/PyInstaller'), '--workpath', path('target/PyInstaller'), SETTINGS['main_module'] ] if debug: cmdline.append('--debug') run(cmdline, check=True) output_dir = path('target/' + app_name + ('.app' if is_mac() else '')) rename(output_dir, path('${freeze_dir}'))
def create_repo_fedora(): if exists(path('target/repo')): rmtree(path('target/repo')) makedirs(path('target/repo/${version}')) copy(path('target/${installer}'), path('target/repo/${version}')) check_call(['createrepo_c', '.'], cwd=(path('target/repo')), stdout=DEVNULL) repo_file = path('src/repo/fedora/${app_name}.repo') use_default = not exists(repo_file) if use_default: repo_file = default_path('src/repo/fedora/AppName.repo') copy_with_filtering(repo_file, path('target/repo'), files_to_filter=[repo_file]) if use_default: rename(path('target/repo/AppName.repo'), path('target/repo/${app_name}.repo'))
def generate_resources(dest_dir=None, dest_dir_for_base=None, exclude=None): if dest_dir is None: # Set this default here instead of in the function definition # (`def generate_resources(dest_dir=path(...) ...)`) because we can't # call path(...) at the module level. dest_dir = path('target/resources') if dest_dir_for_base is None: dest_dir_for_base = dest_dir if exclude is None: exclude = [] resources_to_filter = SETTINGS['resources_to_filter'] kwargs = {'exclude': exclude, 'files_to_filter': resources_to_filter} copy_with_filtering( path('src/main/resources/base'), dest_dir_for_base, **kwargs ) os_resources_dir = path('src/main/resources/' + platform.name().lower()) if exists(os_resources_dir): copy_with_filtering(os_resources_dir, dest_dir, **kwargs)
def get_icons(): result = {} for icons_dir in ( 'src/main/icons/base', 'src/main/icons/' + platform.name().lower() ): for icon_path in glob(path(icons_dir + '/*.png')): size = int(splitext(basename(icon_path))[0]) result[size] = icon_path return list(result.items())
def gengpgkey(): """ Generate a GPG key for Linux code signing """ require_existing_project() if exists(_DEST_DIR): raise FbsError('The %s folder already exists. Aborting.' % _DEST_DIR) try: email = prompt_for_value('Email address') name = prompt_for_value('Real name', default=SETTINGS['author']) passphrase = prompt_for_value('Key password', password=True) except KeyboardInterrupt: print('') return print('') _LOG.info('Generating the GPG key. This can take a little...') _init_docker() args = ['run', '-t'] if exists('/dev/urandom'): # Give the key generator more entropy on Posix: args.extend(['-v', '/dev/urandom:/dev/random']) args.extend([_DOCKER_IMAGE, '/root/genkey.sh', name, email, passphrase]) result = _run_docker(args, check=True, stdout=PIPE, universal_newlines=True) key = _snip( result.stdout, "revocation certificate stored as '/root/.gnupg/openpgp-revocs.d/", ".rev'", include_bounds=False) pubkey = _snip(result.stdout, '-----BEGIN PGP PUBLIC KEY BLOCK-----\n', '-----END PGP PUBLIC KEY BLOCK-----\n') privkey = _snip(result.stdout, '-----BEGIN PGP PRIVATE KEY BLOCK-----\n', '-----END PGP PRIVATE KEY BLOCK-----\n') makedirs(path(_DEST_DIR), exist_ok=True) pubkey_dest = _DEST_DIR + '/' + _PUBKEY_NAME Path(path(pubkey_dest)).write_text(pubkey) Path(path(_DEST_DIR + '/' + _PRIVKEY_NAME)).write_text(privkey) update_json(path(BASE_JSON), {'gpg_key': key, 'gpg_name': name}) update_json(path(SECRET_JSON), {'gpg_pass': passphrase}) _LOG.info( 'Done. Created %s and ...%s. Also updated %s and ...secret.json with ' 'the values you provided.', pubkey_dest, _PRIVKEY_NAME, BASE_JSON)
def update_json(f_path, dict_): f = Path(f_path) try: contents = f.read_text() except FileNotFoundError: indent = _infer_indent(Path(path(BASE_JSON)).read_text()) new_contents = json.dumps(dict_, indent=indent) else: new_contents = _update_json_str(contents, dict_) f.write_text(new_contents)
def clean(): """ Remove previous build outputs """ try: rmtree(path('target')) except FileNotFoundError: return except OSError: # In a docker container, target/ may be mounted so we can't delete it. # Delete its contents instead: for f in listdir(path('target')): fpath = join(path('target'), f) if isdir(fpath): rmtree(fpath, ignore_errors=True) elif isfile(fpath): remove(fpath) elif islink(fpath): unlink(fpath)
def installer(): """ Create an installer for your app """ require_existing_project() if not exists(path('${freeze_dir}')): raise FbsError( 'It seems your app has not yet been frozen. Please run:\n' ' fbs freeze') linux_distribution_not_supported_msg = \ "Your Linux distribution is not supported, sorry. " \ "You can run `fbs buildvm` followed by `fbs runvm` to start a Docker " \ "VM of a supported distribution." try: installer_fname = SETTINGS['installer'] except KeyError: if is_linux(): raise FbsError(linux_distribution_not_supported_msg) raise out_file = join('target', installer_fname) msg_parts = ['Created %s.' % out_file] if is_windows(): from fbs.installer.windows import create_installer_windows create_installer_windows() elif is_mac(): from fbs.installer.mac import create_installer_mac create_installer_mac() elif is_linux(): app_name = SETTINGS['app_name'] if is_ubuntu(): from fbs.installer.ubuntu import create_installer_ubuntu create_installer_ubuntu() install_cmd = 'sudo dpkg -i ' + out_file remove_cmd = 'sudo dpkg --purge ' + app_name elif is_arch_linux(): from fbs.installer.arch import create_installer_arch create_installer_arch() install_cmd = 'sudo pacman -U ' + out_file remove_cmd = 'sudo pacman -R ' + app_name elif is_fedora(): from fbs.installer.fedora import create_installer_fedora create_installer_fedora() install_cmd = 'sudo dnf install ' + out_file remove_cmd = 'sudo dnf remove ' + app_name else: raise FbsError(linux_distribution_not_supported_msg) msg_parts.append( 'You can for instance install it via the following command:\n' ' %s\n' 'This places it in /opt/%s. To uninstall it again, you can use:\n' ' %s' % (install_cmd, app_name, remove_cmd)) else: raise FbsError('Unsupported OS') _LOG.info(' '.join(msg_parts))
def run(): """ Run your app from source """ env = dict(os.environ) pythonpath = path('src/main/python') old_pythonpath = env.get('PYTHONPATH', '') if old_pythonpath: pythonpath += os.pathsep + old_pythonpath env['PYTHONPATH'] = pythonpath subprocess.run([sys.executable, SETTINGS['main_module']], env=env)
def generate_resources(): """ Copy the data files from src/main/resources to the target/ directory. Automatically filters files mentioned in the setting resources_to_filter: Placeholders such as ${app_name} are automatically replaced by the corresponding setting in files on that list. """ freeze_dir = path('${freeze_dir}') if is_mac(): resources_dest_dir = join(freeze_dir, 'Contents', 'Resources') else: resources_dest_dir = freeze_dir kwargs = {'files_to_filter': SETTINGS['resources_to_filter']} resource_dirs = (path('src/main/resources/base'), path('src/main/resources/' + platform.name().lower())) for dir_ in resource_dirs: if exists(dir_): copy_with_filtering(dir_, resources_dest_dir, **kwargs) frozen_resources_dir = dir_ + '-frozen' if exists(frozen_resources_dir): copy_with_filtering(frozen_resources_dir, freeze_dir, **kwargs)
def get_icons(): """ Return a list [(size_in_pixels, path)] of available app icons for the current platform. """ result = {} for icons_dir in ('src/main/icons/base', 'src/main/icons/' + platform.name().lower()): for icon_path in glob(path(icons_dir + '/*.png')): size = int(splitext(basename(icon_path))[0]) result[size] = icon_path return list(result.items())