summaryrefslogtreecommitdiff
path: root/scripts/dev/build_release.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/dev/build_release.py')
-rwxr-xr-xscripts/dev/build_release.py285
1 files changed, 110 insertions, 175 deletions
diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py
index 996487693..65eef720c 100755
--- a/scripts/dev/build_release.py
+++ b/scripts/dev/build_release.py
@@ -1,20 +1,8 @@
#!/usr/bin/env python3
-# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
-#
-# This file is part of qutebrowser.
-#
-# qutebrowser is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# qutebrowser 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 General Public License for more details.
+
+# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
-# You should have received a copy of the GNU General Public License
-# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
+# SPDX-License-Identifier: GPL-3.0-or-later
"""Build a new release."""
@@ -131,12 +119,12 @@ def _smoke_test_run(
'--temp-basedir',
*args,
'about:blank',
- ':later 500 quit',
+ ':cmd-later 500 quit',
]
return subprocess.run(argv, check=True, capture_output=True)
-def smoke_test(executable: pathlib.Path, debug: bool, qt6: bool) -> None:
+def smoke_test(executable: pathlib.Path, debug: bool, qt5: bool) -> None:
"""Try starting the given qutebrowser executable."""
stdout_whitelist = []
stderr_whitelist = [
@@ -176,16 +164,13 @@ def smoke_test(executable: pathlib.Path, debug: bool, qt6: bool) -> None:
r'ContextResult::kTransientFailure: Failed to send '
r'.*CreateCommandBuffer\.'),
])
- if qt6:
+ if not qt5:
stderr_whitelist.extend([
# FIXME:qt6 Qt 6.3 on macOS
r'[0-9:]* WARNING: Incompatible version of OpenSSL',
r'[0-9:]* WARNING: Qt WebEngine resources not found at .*',
(r'[0-9:]* WARNING: Installed Qt WebEngine locales directory not found at '
r'location /qtwebengine_locales\. Trying application directory\.\.\.'),
-
- # https://github.com/pyinstaller/pyinstaller/pull/6903
- r"[0-9:]* INFO: Sandboxing disabled by user\.",
])
elif IS_WINDOWS:
stderr_whitelist.extend([
@@ -257,66 +242,11 @@ def verify_windows_exe(exe_path: pathlib.Path) -> None:
assert pe.verify_checksum()
-def patch_mac_app(qt6: bool) -> None:
- """Patch .app to save some space and make it signable."""
- dist_path = pathlib.Path('dist')
- ver = '6' if qt6 else '5'
- app_path = dist_path / 'qutebrowser.app'
-
- contents_path = app_path / 'Contents'
- macos_path = contents_path / 'MacOS'
- resources_path = contents_path / 'Resources'
- pyqt_path = macos_path / f'PyQt{ver}'
-
- # Replace some duplicate files by symlinks
- framework_path = pyqt_path / f'Qt{ver}' / 'lib' / 'QtWebEngineCore.framework'
-
- framework_resource_path = framework_path / 'Resources'
- for file_path in framework_resource_path.iterdir():
- target = pathlib.Path(*[os.pardir] * 5, file_path.name)
- if file_path.is_dir():
- shutil.rmtree(file_path)
- else:
- file_path.unlink()
- file_path.symlink_to(target)
-
- if qt6:
- # Symlinking QtWebEngineCore.framework does not seem to work with Qt 6.
- # Also, the symlinking/moving before signing doesn't seem to be required.
- return
-
- core_lib = framework_path / 'Versions' / '5' / 'QtWebEngineCore'
- core_lib.unlink()
- core_target = pathlib.Path(*[os.pardir] * 7, 'MacOS', 'QtWebEngineCore')
- core_lib.symlink_to(core_target)
-
- # Move stuff around to make things signable on macOS
- # See https://github.com/pyinstaller/pyinstaller/issues/6612
- pyqt_path_dest = resources_path / pyqt_path.name
- shutil.move(pyqt_path, pyqt_path_dest)
- pyqt_path_target = pathlib.Path("..") / pyqt_path_dest.relative_to(contents_path)
- pyqt_path.symlink_to(pyqt_path_target)
-
- for path in macos_path.glob("Qt*"):
- link_path = resources_path / path.name
- target_path = pathlib.Path("..") / path.relative_to(contents_path)
- link_path.symlink_to(target_path)
-
-
-def sign_mac_app() -> None:
+def verify_mac_app() -> None:
"""Re-sign and verify the Mac .app."""
app_path = pathlib.Path('dist') / 'qutebrowser.app'
subprocess.run([
'codesign',
- '-s', '-',
- '--force',
- '--timestamp',
- '--deep',
- '--verbose',
- app_path,
- ], check=True)
- subprocess.run([
- 'codesign',
'--verify',
'--strict',
'--deep',
@@ -333,7 +263,7 @@ def _mac_bin_path(base: pathlib.Path) -> pathlib.Path:
def build_mac(
*,
gh_token: Optional[str],
- qt6: bool,
+ qt5: bool,
skip_packaging: bool,
debug: bool,
) -> List[Artifact]:
@@ -348,20 +278,18 @@ def build_mac(
shutil.rmtree(d, ignore_errors=True)
utils.print_title("Updating 3rdparty content")
- update_3rdparty.run(ace=False, pdfjs=True, legacy_pdfjs=not qt6, fancy_dmg=False,
+ update_3rdparty.run(ace=False, pdfjs=True, legacy_pdfjs=qt5, fancy_dmg=False,
gh_token=gh_token)
utils.print_title("Building .app via pyinstaller")
- call_tox(f'pyinstaller-64bit{"-qt6" if qt6 else ""}', '-r', debug=debug)
- utils.print_title("Patching .app")
- patch_mac_app(qt6=qt6)
- utils.print_title("Re-signing .app")
- sign_mac_app()
+ call_tox(f'pyinstaller{"-qt5" if qt5 else ""}', '-r', debug=debug)
+ utils.print_title("Verifying .app")
+ verify_mac_app()
dist_path = pathlib.Path("dist")
utils.print_title("Running pre-dmg smoke test")
- smoke_test(_mac_bin_path(dist_path), debug=debug, qt6=qt6)
+ smoke_test(_mac_bin_path(dist_path), debug=debug, qt5=qt5)
if skip_packaging:
return []
@@ -371,7 +299,7 @@ def build_mac(
subprocess.run(['make', '-f', dmg_makefile_path], check=True)
suffix = "-debug" if debug else ""
- suffix += "-qt6" if qt6 else ""
+ suffix += "-qt5" if qt5 else ""
dmg_path = dist_path / f'qutebrowser-{qutebrowser.__version__}{suffix}.dmg'
pathlib.Path('qutebrowser.dmg').rename(dmg_path)
@@ -383,7 +311,7 @@ def build_mac(
subprocess.run(['hdiutil', 'attach', dmg_path,
'-mountpoint', tmp_path], check=True)
try:
- smoke_test(_mac_bin_path(tmp_path), debug=debug, qt6=qt6)
+ smoke_test(_mac_bin_path(tmp_path), debug=debug, qt5=qt5)
finally:
print("Waiting 10s for dmg to be detachable...")
time.sleep(10)
@@ -400,18 +328,14 @@ def build_mac(
]
-def _get_windows_python_path(x64: bool) -> pathlib.Path:
+def _get_windows_python_path() -> pathlib.Path:
"""Get the path to Python.exe on Windows."""
parts = str(sys.version_info.major), str(sys.version_info.minor)
ver = ''.join(parts)
dot_ver = '.'.join(parts)
- if x64:
- path = rf'SOFTWARE\Python\PythonCore\{dot_ver}\InstallPath'
- fallback = pathlib.Path('C:', f'Python{ver}', 'python.exe')
- else:
- path = rf'SOFTWARE\WOW6432Node\Python\PythonCore\{dot_ver}-32\InstallPath'
- fallback = pathlib.Path('C:', f'Python{ver}-32', 'python.exe')
+ path = rf'SOFTWARE\Python\PythonCore\{dot_ver}\InstallPath'
+ fallback = pathlib.Path('C:', f'Python{ver}', 'python.exe')
try:
key = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE, path)
@@ -421,139 +345,114 @@ def _get_windows_python_path(x64: bool) -> pathlib.Path:
def _build_windows_single(
- *, x64: bool,
- qt6: bool,
+ *,
+ qt5: bool,
skip_packaging: bool,
debug: bool,
) -> List[Artifact]:
- """Build on Windows for a single architecture."""
- human_arch = '64-bit' if x64 else '32-bit'
- utils.print_title(f"Running pyinstaller {human_arch}")
+ """Build on Windows for a single build type."""
+ utils.print_title("Running pyinstaller")
dist_path = pathlib.Path("dist")
- arch = "x64" if x64 else "x86"
- out_path = dist_path / f'qutebrowser-{qutebrowser.__version__}-{arch}'
+ out_path = dist_path / f'qutebrowser-{qutebrowser.__version__}'
_maybe_remove(out_path)
- python = _get_windows_python_path(x64=x64)
- suffix = "64bit" if x64 else "32bit"
- if qt6:
- # FIXME:qt6 does this regress 391623d5ec983ecfc4512c7305c4b7a293ac3872?
- suffix += "-qt6"
- call_tox(f'pyinstaller-{suffix}', '-r', python=python, debug=debug)
+ python = _get_windows_python_path()
+ # FIXME:qt6 does this regress 391623d5ec983ecfc4512c7305c4b7a293ac3872?
+ suffix = "-qt5" if qt5 else ""
+ call_tox(f'pyinstaller{suffix}', '-r', python=python, debug=debug)
out_pyinstaller = dist_path / "qutebrowser"
shutil.move(out_pyinstaller, out_path)
exe_path = out_path / 'qutebrowser.exe'
- utils.print_title(f"Verifying {human_arch} exe")
+ utils.print_title("Verifying exe")
verify_windows_exe(exe_path)
- utils.print_title(f"Running {human_arch} smoke test")
- smoke_test(exe_path, debug=debug, qt6=qt6)
+ utils.print_title("Running smoke test")
+ smoke_test(exe_path, debug=debug, qt5=qt5)
if skip_packaging:
return []
- utils.print_title(f"Packaging {human_arch}")
+ utils.print_title("Packaging")
return _package_windows_single(
- nsis_flags=[] if x64 else ['/DX86'],
out_path=out_path,
- filename_arch='amd64' if x64 else 'win32',
- desc_arch=human_arch,
- desc_suffix='' if x64 else ' (only for 32-bit Windows!)',
debug=debug,
- qt6=qt6,
+ qt5=qt5,
)
def build_windows(
*, gh_token: str,
skip_packaging: bool,
- only_32bit: bool,
- only_64bit: bool,
- qt6: bool,
+ qt5: bool,
debug: bool,
) -> List[Artifact]:
"""Build windows executables/setups."""
utils.print_title("Updating 3rdparty content")
- update_3rdparty.run(nsis=True, ace=False, pdfjs=True, legacy_pdfjs=not qt6,
+ update_3rdparty.run(nsis=True, ace=False, pdfjs=True, legacy_pdfjs=qt5,
fancy_dmg=False, gh_token=gh_token)
utils.print_title("Building Windows binaries")
- artifacts = []
-
from scripts.dev import gen_versioninfo
utils.print_title("Updating VersionInfo file")
gen_versioninfo.main()
- if not only_32bit:
- artifacts += _build_windows_single(
- x64=True,
- skip_packaging=skip_packaging,
- debug=debug,
- qt6=qt6,
- )
- if not only_64bit and not qt6:
- artifacts += _build_windows_single(
- x64=False,
- skip_packaging=skip_packaging,
- debug=debug,
- qt6=qt6,
- )
-
+ artifacts = _build_windows_single(
+ skip_packaging=skip_packaging,
+ debug=debug,
+ qt5=qt5,
+ )
return artifacts
def _package_windows_single(
*,
- nsis_flags: List[str],
out_path: pathlib.Path,
- desc_arch: str,
- desc_suffix: str,
- filename_arch: str,
debug: bool,
- qt6: bool,
+ qt5: bool,
) -> List[Artifact]:
"""Build the given installer/zip for windows."""
artifacts = []
dist_path = pathlib.Path("dist")
- utils.print_subtitle(f"Building {desc_arch} installer...")
+ utils.print_subtitle("Building installer...")
subprocess.run(['makensis.exe',
- f'/DVERSION={qutebrowser.__version__}', *nsis_flags,
+ f'/DVERSION={qutebrowser.__version__}',
+ f'/DQT5={qt5}',
'misc/nsis/qutebrowser.nsi'], check=True)
name_parts = [
'qutebrowser',
str(qutebrowser.__version__),
- filename_arch,
]
if debug:
name_parts.append('debug')
- if qt6:
- name_parts.append('qt6')
+ if qt5:
+ name_parts.append('qt5')
+
+ name_parts.append('amd64') # FIXME:qt6 temporary until new installer
name = '-'.join(name_parts) + '.exe'
artifacts.append(Artifact(
path=dist_path / name,
mimetype='application/vnd.microsoft.portable-executable',
- description=f'Windows {desc_arch} installer{desc_suffix}',
+ description='Windows installer',
))
- utils.print_subtitle(f"Zipping {desc_arch} standalone...")
+ utils.print_subtitle("Zipping standalone...")
zip_name_parts = [
'qutebrowser',
str(qutebrowser.__version__),
'windows',
'standalone',
- filename_arch,
]
if debug:
zip_name_parts.append('debug')
- if qt6:
- zip_name_parts.append('qt6')
+ if qt5:
+ zip_name_parts.append('qt5')
zip_name = '-'.join(zip_name_parts) + '.zip'
zip_path = dist_path / zip_name
@@ -561,7 +460,7 @@ def _package_windows_single(
artifacts.append(Artifact(
path=zip_path,
mimetype='application/zip',
- description=f'Windows {desc_arch} standalone{desc_suffix}',
+ description='Windows standalone',
))
return artifacts
@@ -619,9 +518,17 @@ def build_sdist() -> List[Artifact]:
def test_makefile() -> None:
"""Make sure the Makefile works correctly."""
utils.print_title("Testing makefile")
+ a2x_path = pathlib.Path(sys.executable).parent / 'a2x'
+ assert a2x_path.exists(), a2x_path
with tempfile.TemporaryDirectory() as tmpdir:
- subprocess.run(['make', '-f', 'misc/Makefile',
- f'DESTDIR={tmpdir}', 'install'], check=True)
+ subprocess.run(
+ [
+ 'make', '-f', 'misc/Makefile',
+ f'DESTDIR={tmpdir}', f'A2X={a2x_path}',
+ 'install'
+ ],
+ check=True,
+ )
def read_github_token(
@@ -632,6 +539,9 @@ def read_github_token(
if arg_token is not None:
return arg_token
+ if "GITHUB_TOKEN" in os.environ:
+ return os.environ["GITHUB_TOKEN"]
+
token_path = pathlib.Path.home() / '.gh_token'
if not token_path.exists():
if optional:
@@ -645,13 +555,19 @@ def read_github_token(
return token
-def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -> None:
+def github_upload(
+ artifacts: List[Artifact],
+ tag: str,
+ gh_token: str,
+ experimental: bool,
+) -> None:
"""Upload the given artifacts to GitHub.
Args:
artifacts: A list of Artifacts to upload.
tag: The name of the release tag
gh_token: The GitHub token to use
+ experimental: Upload to the experiments repo
"""
# pylint: disable=broad-exception-raised
import github3
@@ -659,14 +575,20 @@ def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -> None:
utils.print_title("Uploading to github...")
gh = github3.login(token=gh_token)
- repo = gh.repository('qutebrowser', 'qutebrowser')
+
+ if experimental:
+ repo = gh.repository('qutebrowser', 'experiments')
+ else:
+ repo = gh.repository('qutebrowser', 'qutebrowser')
release = None # to satisfy pylint
for release in repo.releases():
if release.tag_name == tag:
break
else:
- raise Exception(f"No release found for {tag!r}!")
+ releases = ", ".join(r.tag_name for r in repo.releases())
+ raise Exception(
+ f"No release found for {tag!r} in {repo.full_name}, found: {releases}")
for artifact in artifacts:
while True:
@@ -676,6 +598,10 @@ def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -> None:
if asset.name == artifact.path.name]
if assets:
print(f"Assets already exist: {assets}")
+
+ if utils.ON_CI:
+ sys.exit(1)
+
print("Press enter to continue anyways or Ctrl-C to abort.")
input()
@@ -689,8 +615,13 @@ def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -> None:
)
except github3.exceptions.ConnectionError as e:
utils.print_error(f'Failed to upload: {e}')
- print("Press Enter to retry...", file=sys.stderr)
- input()
+ if utils.ON_CI:
+ print("Retrying in 30s...")
+ time.sleep(30)
+ else:
+ print("Press Enter to retry...", file=sys.stderr)
+ input()
+
print("Retrying!")
assets = [asset for asset in release.assets()
@@ -703,10 +634,16 @@ def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -> None:
break
-def pypi_upload(artifacts: List[Artifact]) -> None:
+def pypi_upload(artifacts: List[Artifact], experimental: bool) -> None:
"""Upload the given artifacts to PyPI using twine."""
+ # https://blog.pypi.org/posts/2023-05-23-removing-pgp/
+ artifacts = [a for a in artifacts if a.mimetype != 'application/pgp-signature']
+
utils.print_title("Uploading to PyPI...")
- run_twine('upload', artifacts)
+ if experimental:
+ run_twine('upload', artifacts, "-r", "testpypi")
+ else:
+ run_twine('upload', artifacts)
def twine_check(artifacts: List[Artifact]) -> None:
@@ -732,14 +669,12 @@ def main() -> None:
help="Skip confirmation before uploading.")
parser.add_argument('--skip-packaging', action='store_true', required=False,
help="Skip Windows installer/zip generation or macOS DMG.")
- parser.add_argument('--32bit', action='store_true', required=False,
- help="Skip Windows 64 bit build.", dest='only_32bit')
- parser.add_argument('--64bit', action='store_true', required=False,
- help="Skip Windows 32 bit build.", dest='only_64bit')
parser.add_argument('--debug', action='store_true', required=False,
help="Build a debug build.")
- parser.add_argument('--qt6', action='store_true', required=False,
- help="Build against PyQt6")
+ parser.add_argument('--qt5', action='store_true', required=False,
+ help="Build against PyQt5")
+ parser.add_argument('--experimental', action='store_true', required=False,
+ help="Upload to experiments repo and test PyPI")
args = parser.parse_args()
utils.change_cwd()
@@ -752,6 +687,7 @@ def main() -> None:
gh_token = read_github_token(args.gh_token)
else:
gh_token = read_github_token(args.gh_token, optional=True)
+ assert not args.experimental # makes no sense without upload
if not misc_checks.check_git():
utils.print_error("Refusing to do a release with a dirty git tree")
@@ -766,16 +702,14 @@ def main() -> None:
artifacts = build_windows(
gh_token=gh_token,
skip_packaging=args.skip_packaging,
- only_32bit=args.only_32bit,
- only_64bit=args.only_64bit,
- qt6=args.qt6,
+ qt5=args.qt5,
debug=args.debug,
)
elif IS_MACOS:
artifacts = build_mac(
gh_token=gh_token,
skip_packaging=args.skip_packaging,
- qt6=args.qt6,
+ qt5=args.qt5,
debug=args.debug,
)
else:
@@ -787,14 +721,15 @@ def main() -> None:
if args.upload:
version_tag = f"v{qutebrowser.__version__}"
- if not args.no_confirm:
+ if not args.no_confirm and not utils.ON_CI:
utils.print_title(f"Press enter to release {version_tag}...")
input()
assert gh_token is not None
- github_upload(artifacts, version_tag, gh_token=gh_token)
+ github_upload(
+ artifacts, version_tag, gh_token=gh_token, experimental=args.experimental)
if upload_to_pypi:
- pypi_upload(artifacts)
+ pypi_upload(artifacts, experimental=args.experimental)
else:
print()
utils.print_title("Artifacts")