2019-09-10 00:54:28 +00:00
|
|
|
# Created By: Virgil Dupras
|
|
|
|
# Created On: 2009-03-03
|
|
|
|
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
|
|
|
|
|
|
|
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
|
|
|
# which should be included with this package. The terms are also available at
|
|
|
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
|
|
|
|
|
|
|
"""This module is a collection of function to help in HS apps build process.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import os.path as op
|
|
|
|
import shutil
|
|
|
|
import tempfile
|
|
|
|
import plistlib
|
|
|
|
from subprocess import Popen
|
|
|
|
import re
|
|
|
|
import importlib
|
|
|
|
from datetime import datetime
|
|
|
|
import glob
|
|
|
|
|
|
|
|
from .plat import ISWINDOWS
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
|
|
|
|
def print_and_do(cmd):
|
2021-08-15 08:51:27 +00:00
|
|
|
"""Prints ``cmd`` and executes it in the shell."""
|
2019-09-10 00:54:28 +00:00
|
|
|
print(cmd)
|
|
|
|
p = Popen(cmd, shell=True)
|
|
|
|
return p.wait()
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def _perform(src, dst, action, actionname):
|
|
|
|
if not op.lexists(src):
|
|
|
|
print("Copying %s failed: it doesn't exist." % src)
|
|
|
|
return
|
|
|
|
if op.lexists(dst):
|
|
|
|
if op.isdir(dst):
|
|
|
|
shutil.rmtree(dst)
|
|
|
|
else:
|
|
|
|
os.remove(dst)
|
2022-04-28 01:53:12 +00:00
|
|
|
print("{} {} --> {}".format(actionname, src, dst))
|
2019-09-10 00:54:28 +00:00
|
|
|
action(src, dst)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def copy_file_or_folder(src, dst):
|
|
|
|
if op.isdir(src):
|
|
|
|
shutil.copytree(src, dst, symlinks=True)
|
|
|
|
else:
|
|
|
|
shutil.copy(src, dst)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def move(src, dst):
|
2020-01-01 02:16:27 +00:00
|
|
|
_perform(src, dst, os.rename, "Moving")
|
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
|
|
|
|
def copy(src, dst):
|
2020-01-01 02:16:27 +00:00
|
|
|
_perform(src, dst, copy_file_or_folder, "Copying")
|
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
|
|
|
|
def symlink(src, dst):
|
2020-01-01 02:16:27 +00:00
|
|
|
_perform(src, dst, os.symlink, "Symlinking")
|
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
|
|
|
|
def hardlink(src, dst):
|
2020-01-01 02:16:27 +00:00
|
|
|
_perform(src, dst, os.link, "Hardlinking")
|
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
|
|
|
|
def _perform_on_all(pattern, dst, action):
|
|
|
|
# pattern is a glob pattern, example "folder/foo*". The file is moved directly in dst, no folder
|
|
|
|
# structure from src is kept.
|
|
|
|
filenames = glob.glob(pattern)
|
|
|
|
for fn in filenames:
|
|
|
|
destpath = op.join(dst, op.basename(fn))
|
|
|
|
action(fn, destpath)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def move_all(pattern, dst):
|
|
|
|
_perform_on_all(pattern, dst, move)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def copy_all(pattern, dst):
|
|
|
|
_perform_on_all(pattern, dst, copy)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def ensure_empty_folder(path):
|
2021-08-15 08:51:27 +00:00
|
|
|
"""Make sure that the path exists and that it's an empty folder."""
|
2019-09-10 00:54:28 +00:00
|
|
|
if op.exists(path):
|
|
|
|
shutil.rmtree(path)
|
|
|
|
os.mkdir(path)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def filereplace(filename, outfilename=None, **kwargs):
|
2021-08-15 08:51:27 +00:00
|
|
|
"""Reads `filename`, replaces all {variables} in kwargs, and writes the result to `outfilename`."""
|
2019-09-10 00:54:28 +00:00
|
|
|
if outfilename is None:
|
|
|
|
outfilename = filename
|
2022-04-28 01:53:12 +00:00
|
|
|
fp = open(filename, encoding="utf-8")
|
2019-09-10 00:54:28 +00:00
|
|
|
contents = fp.read()
|
|
|
|
fp.close()
|
|
|
|
# We can't use str.format() because in some files, there might be {} characters that mess with it.
|
|
|
|
for key, item in kwargs.items():
|
2022-04-28 01:53:12 +00:00
|
|
|
contents = contents.replace(f"{{{key}}}", item)
|
2020-01-01 02:16:27 +00:00
|
|
|
fp = open(outfilename, "wt", encoding="utf-8")
|
2019-09-10 00:54:28 +00:00
|
|
|
fp.write(contents)
|
|
|
|
fp.close()
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def get_module_version(modulename):
|
|
|
|
mod = importlib.import_module(modulename)
|
|
|
|
return mod.__version__
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def setup_package_argparser(parser):
|
|
|
|
parser.add_argument(
|
2020-01-01 02:16:27 +00:00
|
|
|
"--sign",
|
|
|
|
dest="sign_identity",
|
|
|
|
help="Sign app under specified identity before packaging (OS X only)",
|
2019-09-10 00:54:28 +00:00
|
|
|
)
|
|
|
|
parser.add_argument(
|
2020-01-01 02:16:27 +00:00
|
|
|
"--nosign",
|
|
|
|
action="store_true",
|
|
|
|
dest="nosign",
|
|
|
|
help="Don't sign the packaged app (OS X only)",
|
2019-09-10 00:54:28 +00:00
|
|
|
)
|
|
|
|
parser.add_argument(
|
2020-01-01 02:16:27 +00:00
|
|
|
"--src-pkg",
|
|
|
|
action="store_true",
|
|
|
|
dest="src_pkg",
|
|
|
|
help="Build a tar.gz of the current source.",
|
2019-09-10 00:54:28 +00:00
|
|
|
)
|
|
|
|
parser.add_argument(
|
2020-01-01 02:16:27 +00:00
|
|
|
"--arch-pkg",
|
|
|
|
action="store_true",
|
|
|
|
dest="arch_pkg",
|
|
|
|
help="Force Arch Linux packaging type, regardless of distro name.",
|
2019-09-10 00:54:28 +00:00
|
|
|
)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
# `args` come from an ArgumentParser updated with setup_package_argparser()
|
|
|
|
def package_cocoa_app_in_dmg(app_path, destfolder, args):
|
|
|
|
# Rather than signing our app in XCode during the build phase, we sign it during the package
|
|
|
|
# phase because running the app before packaging can modify it and we want to be sure to have
|
|
|
|
# a valid signature.
|
|
|
|
if args.sign_identity:
|
2022-04-28 01:53:12 +00:00
|
|
|
sign_identity = f"Developer ID Application: {args.sign_identity}"
|
|
|
|
result = print_and_do(f'codesign --force --deep --sign "{sign_identity}" "{app_path}"')
|
2019-09-10 00:54:28 +00:00
|
|
|
if result != 0:
|
|
|
|
print("ERROR: Signing failed. Aborting packaging.")
|
|
|
|
return
|
|
|
|
elif not args.nosign:
|
|
|
|
print("ERROR: Either --nosign or --sign argument required.")
|
|
|
|
return
|
|
|
|
build_dmg(app_path, destfolder)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def build_dmg(app_path, destfolder):
|
|
|
|
"""Builds a DMG volume with application at ``app_path`` and puts it in ``dest_path``.
|
|
|
|
|
|
|
|
The name of the resulting DMG volume is determined by the app's name and version.
|
|
|
|
"""
|
2020-01-01 02:16:27 +00:00
|
|
|
print(repr(op.join(app_path, "Contents", "Info.plist")))
|
|
|
|
plist = plistlib.readPlist(op.join(app_path, "Contents", "Info.plist"))
|
2019-09-10 00:54:28 +00:00
|
|
|
workpath = tempfile.mkdtemp()
|
2020-01-01 02:16:27 +00:00
|
|
|
dmgpath = op.join(workpath, plist["CFBundleName"])
|
2019-09-10 00:54:28 +00:00
|
|
|
os.mkdir(dmgpath)
|
2022-04-28 01:53:12 +00:00
|
|
|
print_and_do('cp -R "{}" "{}"'.format(app_path, dmgpath))
|
2020-01-01 02:16:27 +00:00
|
|
|
print_and_do('ln -s /Applications "%s"' % op.join(dmgpath, "Applications"))
|
2022-04-28 01:53:12 +00:00
|
|
|
dmgname = "{}_osx_{}.dmg".format(
|
2020-01-01 02:16:27 +00:00
|
|
|
plist["CFBundleName"].lower().replace(" ", "_"),
|
|
|
|
plist["CFBundleVersion"].replace(".", "_"),
|
|
|
|
)
|
|
|
|
print("Building %s" % dmgname)
|
2019-09-10 00:54:28 +00:00
|
|
|
# UDBZ = bzip compression. UDZO (zip compression) was used before, but it compresses much less.
|
2022-04-28 01:59:20 +00:00
|
|
|
print_and_do(
|
|
|
|
'hdiutil create "{}" -format UDBZ -nocrossdev -srcdir "{}"'.format(op.join(destfolder, dmgname), dmgpath)
|
|
|
|
)
|
2020-01-01 02:16:27 +00:00
|
|
|
print("Build Complete")
|
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
|
|
|
|
def add_to_pythonpath(path):
|
2021-08-15 08:51:27 +00:00
|
|
|
"""Adds ``path`` to both ``PYTHONPATH`` env and ``sys.path``."""
|
2019-09-10 00:54:28 +00:00
|
|
|
abspath = op.abspath(path)
|
2020-01-01 02:16:27 +00:00
|
|
|
pythonpath = os.environ.get("PYTHONPATH", "")
|
|
|
|
pathsep = ";" if ISWINDOWS else ":"
|
2019-09-10 00:54:28 +00:00
|
|
|
pythonpath = pathsep.join([abspath, pythonpath]) if pythonpath else abspath
|
2020-01-01 02:16:27 +00:00
|
|
|
os.environ["PYTHONPATH"] = pythonpath
|
2019-09-10 00:54:28 +00:00
|
|
|
sys.path.insert(1, abspath)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
# This is a method to hack around those freakingly tricky data inclusion/exlusion rules
|
|
|
|
# in setuptools. We copy the packages *without data* in a build folder and then build the plugin
|
|
|
|
# from there.
|
|
|
|
def copy_packages(packages_names, dest, create_links=False, extra_ignores=None):
|
|
|
|
"""Copy python packages ``packages_names`` to ``dest``, spurious data.
|
|
|
|
|
|
|
|
Copy will happen without tests, testdata, mercurial data or C extension module source with it.
|
|
|
|
``py2app`` include and exclude rules are **quite** funky, and doing this is the only reliable
|
|
|
|
way to make sure we don't end up with useless stuff in our app.
|
|
|
|
"""
|
|
|
|
if ISWINDOWS:
|
|
|
|
create_links = False
|
|
|
|
if not extra_ignores:
|
|
|
|
extra_ignores = []
|
2021-08-15 08:51:27 +00:00
|
|
|
ignore = shutil.ignore_patterns(".hg*", "tests", "testdata", "modules", "docs", "locale", *extra_ignores)
|
2019-09-10 00:54:28 +00:00
|
|
|
for package_name in packages_names:
|
|
|
|
if op.exists(package_name):
|
|
|
|
source_path = package_name
|
|
|
|
else:
|
|
|
|
mod = __import__(package_name)
|
|
|
|
source_path = mod.__file__
|
2020-01-01 02:16:27 +00:00
|
|
|
if mod.__file__.endswith("__init__.py"):
|
2019-09-10 00:54:28 +00:00
|
|
|
source_path = op.dirname(source_path)
|
|
|
|
dest_name = op.basename(source_path)
|
|
|
|
dest_path = op.join(dest, dest_name)
|
|
|
|
if op.exists(dest_path):
|
|
|
|
if op.islink(dest_path):
|
|
|
|
os.unlink(dest_path)
|
|
|
|
else:
|
|
|
|
shutil.rmtree(dest_path)
|
2022-04-28 01:53:12 +00:00
|
|
|
print(f"Copying package at {source_path} to {dest_path}")
|
2019-09-10 00:54:28 +00:00
|
|
|
if create_links:
|
|
|
|
os.symlink(op.abspath(source_path), dest_path)
|
|
|
|
else:
|
|
|
|
if op.isdir(source_path):
|
|
|
|
shutil.copytree(source_path, dest_path, ignore=ignore)
|
|
|
|
else:
|
|
|
|
shutil.copy(source_path, dest_path)
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
|
|
|
def build_debian_changelog(
|
|
|
|
changelogpath,
|
|
|
|
destfile,
|
|
|
|
pkgname,
|
|
|
|
from_version=None,
|
|
|
|
distribution="precise",
|
|
|
|
fix_version=None,
|
|
|
|
):
|
2019-09-10 00:54:28 +00:00
|
|
|
"""Builds a debian changelog out of a YAML changelog.
|
|
|
|
|
|
|
|
Use fix_version to patch the top changelog to that version (if, for example, there was a
|
|
|
|
packaging error and you need to quickly fix it)
|
|
|
|
"""
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def desc2list(desc):
|
|
|
|
# We take each item, enumerated with the '*' character, and transform it into a list.
|
2020-01-01 02:16:27 +00:00
|
|
|
desc = desc.replace("\n", " ")
|
|
|
|
desc = desc.replace(" ", " ")
|
|
|
|
result = desc.split("*")
|
2019-09-10 00:54:28 +00:00
|
|
|
return [s.strip() for s in result if s.strip()]
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
ENTRY_MODEL = (
|
2020-12-29 22:45:15 +00:00
|
|
|
"{pkg} ({version}) {distribution}; urgency=low\n\n{changes}\n "
|
2020-01-01 02:16:27 +00:00
|
|
|
"-- Virgil Dupras <hsoft@hardcoded.net> {date}\n\n"
|
|
|
|
)
|
2019-09-10 00:54:28 +00:00
|
|
|
CHANGE_MODEL = " * {description}\n"
|
|
|
|
changelogs = read_changelog_file(changelogpath)
|
|
|
|
if from_version:
|
|
|
|
# We only want logs from a particular version
|
|
|
|
for index, log in enumerate(changelogs):
|
2020-01-01 02:16:27 +00:00
|
|
|
if log["version"] == from_version:
|
|
|
|
changelogs = changelogs[: index + 1]
|
2019-09-10 00:54:28 +00:00
|
|
|
break
|
|
|
|
if fix_version:
|
2020-01-01 02:16:27 +00:00
|
|
|
changelogs[0]["version"] = fix_version
|
2019-09-10 00:54:28 +00:00
|
|
|
rendered_logs = []
|
|
|
|
for log in changelogs:
|
2020-01-01 02:16:27 +00:00
|
|
|
version = log["version"]
|
|
|
|
logdate = log["date"]
|
|
|
|
desc = log["description"]
|
|
|
|
rendered_date = logdate.strftime("%a, %d %b %Y 00:00:00 +0000")
|
2019-09-10 00:54:28 +00:00
|
|
|
rendered_descs = [CHANGE_MODEL.format(description=d) for d in desc2list(desc)]
|
2020-01-01 02:16:27 +00:00
|
|
|
changes = "".join(rendered_descs)
|
|
|
|
rendered_log = ENTRY_MODEL.format(
|
|
|
|
pkg=pkgname,
|
|
|
|
version=version,
|
|
|
|
changes=changes,
|
|
|
|
date=rendered_date,
|
|
|
|
distribution=distribution,
|
|
|
|
)
|
2019-09-10 00:54:28 +00:00
|
|
|
rendered_logs.append(rendered_log)
|
2020-01-01 02:16:27 +00:00
|
|
|
result = "".join(rendered_logs)
|
|
|
|
fp = open(destfile, "w")
|
2019-09-10 00:54:28 +00:00
|
|
|
fp.write(result)
|
|
|
|
fp.close()
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
|
|
|
re_changelog_header = re.compile(r"=== ([\d.b]*) \(([\d\-]*)\)")
|
|
|
|
|
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def read_changelog_file(filename):
|
|
|
|
def iter_by_three(it):
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
version = next(it)
|
|
|
|
date = next(it)
|
|
|
|
description = next(it)
|
|
|
|
except StopIteration:
|
|
|
|
return
|
|
|
|
yield version, date, description
|
|
|
|
|
2022-04-28 01:53:12 +00:00
|
|
|
with open(filename, encoding="utf-8") as fp:
|
2019-09-10 00:54:28 +00:00
|
|
|
contents = fp.read()
|
2020-01-01 02:16:27 +00:00
|
|
|
splitted = re_changelog_header.split(contents)[1:] # the first item is empty
|
2019-09-10 00:54:28 +00:00
|
|
|
result = []
|
|
|
|
for version, date_str, description in iter_by_three(iter(splitted)):
|
2020-01-01 02:16:27 +00:00
|
|
|
date = datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
|
|
d = {
|
|
|
|
"date": date,
|
|
|
|
"date_str": date_str,
|
|
|
|
"version": version,
|
|
|
|
"description": description.strip(),
|
|
|
|
}
|
2019-09-10 00:54:28 +00:00
|
|
|
result.append(d)
|
|
|
|
return result
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def fix_qt_resource_file(path):
|
|
|
|
# pyrcc5 under Windows, if the locale is non-english, can produce a source file with a date
|
|
|
|
# containing accented characters. If it does, the encoding is wrong and it prevents the file
|
|
|
|
# from being correctly frozen by cx_freeze. To work around that, we open the file, strip all
|
|
|
|
# comments, and save.
|
2020-01-01 02:16:27 +00:00
|
|
|
with open(path, "rb") as fp:
|
2019-09-10 00:54:28 +00:00
|
|
|
contents = fp.read()
|
2020-01-01 02:16:27 +00:00
|
|
|
lines = contents.split(b"\n")
|
2020-06-24 22:11:09 +00:00
|
|
|
lines = [line for line in lines if not line.startswith(b"#")]
|
2020-01-01 02:16:27 +00:00
|
|
|
with open(path, "wb") as fp:
|
|
|
|
fp.write(b"\n".join(lines))
|