Windows: build pyinstaller onedir and use it for installer

This greatly improves startup time as pyinstaller doesn't have to
unpack everything on startup. The same also applies to macOS and Linux,
but there we 1) don't have installers to hide all the files and
2) have a filesystem that deals much better with lots of small files.

Additionally, simplify cibuild to be a bit more reasonable.
This commit is contained in:
Maximilian Hils 2021-01-05 15:56:33 +01:00
parent ca45548289
commit cca242a581
6 changed files with 414 additions and 401 deletions

View File

@ -1,9 +1,7 @@
#!/usr/bin/env python3
import contextlib
import glob
import os
import pathlib
import platform
import re
import shutil
@ -12,15 +10,17 @@ import sys
import tarfile
import urllib.request
import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Union
import click
import parver
import cryptography.fernet
import parver
@contextlib.contextmanager
def chdir(path: str): # pragma: no cover
def chdir(path: Path): # pragma: no cover
old_dir = os.getcwd()
os.chdir(path)
yield
@ -39,6 +39,16 @@ def bool_from_env(envvar: str) -> bool:
return True
class ZipFile2(zipfile.ZipFile):
# ZipFile and tarfile have slightly different APIs. Let's fix that.
def add(self, name: str, arcname: str) -> None:
return self.write(name, arcname)
def __enter__(self) -> "ZipFile2":
return self
@dataclass(frozen=True, repr=False)
class BuildEnviron:
PLATFORM_TAGS = {
"Darwin": "osx",
@ -46,100 +56,89 @@ class BuildEnviron:
"Linux": "linux",
}
def __init__(
self,
*,
system="",
root_dir="",
github_ref="",
github_event_name="",
should_build_wheel=False,
should_build_docker=False,
should_build_pyinstaller=False,
should_build_wininstaller=False,
has_aws_creds=False,
has_twine_creds=False,
docker_username="",
docker_password="",
build_key="",
):
self.system = system
self.root_dir = root_dir
self.should_build_wheel = should_build_wheel
self.should_build_docker = should_build_docker
self.should_build_pyinstaller = should_build_pyinstaller
self.should_build_wininstaller = should_build_wininstaller
self.github_ref = github_ref
self.github_event_name = github_event_name
self.has_aws_creds = has_aws_creds
self.has_twine_creds = has_twine_creds
self.docker_username = docker_username
self.docker_password = docker_password
self.build_key = build_key
system: str
root_dir: Path
branch: Optional[str] = None
tag: Optional[str] = None
is_pull_request: bool = True
should_build_wheel: bool = False
should_build_docker: bool = False
should_build_pyinstaller: bool = False
should_build_wininstaller: bool = False
has_aws_creds: bool = False
has_twine_creds: bool = False
docker_username: Optional[str] = None
docker_password: Optional[str] = None
build_key: Optional[str] = None
@classmethod
def from_env(cls):
def from_env(cls) -> "BuildEnviron":
branch = None
tag = None
if ref := os.environ.get("GITHUB_REF", ""):
if ref.startswith("refs/heads/"):
branch = ref.replace("refs/heads/", "")
if ref.startswith("refs/pull/"):
branch = "pr-" + ref.split("/")[2]
if ref.startswith("refs/tags/"):
tag = ref.replace("refs/tags/", "")
is_pull_request = os.environ.get("GITHUB_EVENT_NAME", "pull_request") == "pull_request"
is_untrusted = (
is_pull_request
or
os.environ.get("GITHUB_REPOSITORY", "other") != "mitmproxy/mitmproxy"
)
has_creds = any(
x in os.environ for x in
["AWS_ACCESS_KEY_ID",
"TWINE_USERNAME", "TWINE_PASSWORD",
"DOCKER_USERNAME", "DOCKER_PASSWORD",
"CI_BUILD_KEY"]
)
if is_untrusted and has_creds:
raise RuntimeError("Found upload credentials even though we aren't running on CI!")
return cls(
system=platform.system(),
root_dir=os.path.normpath(os.path.join(os.path.dirname(__file__), "..")),
github_ref=os.environ.get("GITHUB_REF", ""),
github_event_name=os.environ.get("GITHUB_EVENT_NAME", ""),
root_dir=Path(__file__).parent.parent,
branch=branch,
tag=tag,
is_pull_request=is_pull_request,
should_build_wheel=bool_from_env("CI_BUILD_WHEEL"),
should_build_pyinstaller=bool_from_env("CI_BUILD_PYINSTALLER"),
should_build_wininstaller=bool_from_env("CI_BUILD_WININSTALLER"),
should_build_docker=bool_from_env("CI_BUILD_DOCKER"),
has_aws_creds=bool_from_env("AWS_ACCESS_KEY_ID"),
has_twine_creds=bool_from_env("TWINE_USERNAME") and bool_from_env("TWINE_PASSWORD"),
docker_username=os.environ.get("DOCKER_USERNAME", ""),
docker_password=os.environ.get("DOCKER_PASSWORD", ""),
build_key=os.environ.get("CI_BUILD_KEY", ""),
docker_username=os.environ.get("DOCKER_USERNAME", None),
docker_password=os.environ.get("DOCKER_PASSWORD", None),
build_key=os.environ.get("CI_BUILD_KEY", None),
)
def archive(self, path):
# ZipFile and tarfile have slightly different APIs. Fix that.
def archive(self, path: Path) -> Union[tarfile.TarFile, ZipFile2]:
if self.system == "Windows":
a = zipfile.ZipFile(path, "w")
a.add = a.write
return a
return ZipFile2(path, "w")
else:
return tarfile.open(path, "w:gz")
def archive_name(self, bdist: str) -> str:
@property
def archive_path(self) -> Path:
if self.system == "Windows":
ext = "zip"
else:
ext = "tar.gz"
return "{project}-{version}-{platform}.{ext}".format(
project=bdist,
version=self.version,
platform=self.platform_tag,
ext=ext
)
return self.dist_dir / f"mitmproxy-{self.version}-{self.platform_tag}.{ext}"
@property
def bdists(self):
return {
"mitmproxy": ["mitmproxy", "mitmdump", "mitmweb"],
}
def build_dir(self) -> Path:
return self.release_dir / "build"
@property
def branch(self) -> str:
if self.github_ref and self.github_ref.startswith("refs/heads/"):
return self.github_ref.replace("refs/heads/", "")
if self.github_ref and self.github_ref.startswith("refs/pull/"):
return "pr-" + self.github_ref.split("/")[2]
return ""
@property
def build_dir(self) -> str:
return os.path.join(self.release_dir, "build")
@property
def dist_dir(self) -> str:
return os.path.join(self.release_dir, "dist")
def dist_dir(self) -> Path:
return self.release_dir / "dist"
@property
def docker_tag(self) -> str:
@ -159,7 +158,6 @@ class BuildEnviron:
"release_dir",
"build_dir",
"dist_dir",
"bdists",
"upload_dir",
"should_build_wheel",
"should_build_pyinstaller",
@ -177,8 +175,7 @@ class BuildEnviron:
Check that version numbers match our conventions.
Raises a ValueError if there is a mismatch.
"""
with open(pathlib.Path(self.root_dir) / "mitmproxy" / "version.py") as f:
contents = f.read()
contents = (self.root_dir / "mitmproxy" / "version.py").read_text("utf8")
match = re.search(r'^VERSION = "(.+?)"', contents, re.M)
assert match
version = match.group(1)
@ -211,7 +208,7 @@ class BuildEnviron:
@property
def is_prod_release(self) -> bool:
if not (self.tag and self.tag.startswith("v")):
if not self.tag or not self.tag.startswith("v"):
return False
try:
v = parver.Version.parse(self.version, strict=True)
@ -219,21 +216,15 @@ class BuildEnviron:
return False
return not v.is_prerelease
@property
def is_pull_request(self) -> bool:
if self.github_event_name == "pull_request":
return True
return False
@property
def platform_tag(self) -> str:
if self.system in self.PLATFORM_TAGS:
return self.PLATFORM_TAGS[self.system]
raise BuildError("Unsupported platform: %s" % self.system)
raise BuildError(f"Unsupported platform: {self.system}")
@property
def release_dir(self) -> str:
return os.path.join(self.root_dir, "release")
def release_dir(self) -> Path:
return self.root_dir / "release"
@property
def should_upload_docker(self) -> bool:
@ -258,18 +249,12 @@ class BuildEnviron:
self.has_twine_creds,
])
@property
def tag(self) -> str:
if self.github_ref and self.github_ref.startswith("refs/tags/"):
return self.github_ref.replace("refs/tags/", "")
return ""
@property
def upload_dir(self) -> str:
if self.tag:
return self.version
else:
return "branches/%s" % self.version
return f"branches/{self.version}"
@property
def version(self) -> str:
@ -287,7 +272,7 @@ class BuildEnviron:
raise BuildError("We're on neither a tag nor a branch - could not establish version")
def build_wheel(be: BuildEnviron): # pragma: no cover
def build_wheel(be: BuildEnviron) -> None: # pragma: no cover
click.echo("Building wheel...")
subprocess.check_call([
"python",
@ -296,22 +281,21 @@ def build_wheel(be: BuildEnviron): # pragma: no cover
"bdist_wheel",
"--dist-dir", be.dist_dir,
])
whl, = glob.glob(os.path.join(be.dist_dir, 'mitmproxy-*-py3-none-any.whl'))
whl, = be.dist_dir.glob('mitmproxy-*-py3-none-any.whl')
click.echo(f"Found wheel package: {whl}")
subprocess.check_call(["tox", "-e", "wheeltest", "--", whl])
return whl
def build_docker_image(be: BuildEnviron): # pragma: no cover
whl, = glob.glob(os.path.join(be.dist_dir, 'mitmproxy-*-py3-none-any.whl'))
whl = str(pathlib.Path(whl).relative_to(pathlib.Path(".").absolute()))
def build_docker_image(be: BuildEnviron) -> None: # pragma: no cover
whl, = be.dist_dir.glob('mitmproxy-*-py3-none-any.whl')
whl = whl.relative_to(Path(".").absolute())
click.echo("Building Docker images...")
subprocess.check_call([
"docker",
"build",
"--tag", be.docker_tag,
"--build-arg", f"WHEEL_MITMPROXY={whl}",
"--build-arg", "WHEEL_BASENAME_MITMPROXY={}".format(os.path.basename(whl)),
"--build-arg", f"WHEEL_BASENAME_MITMPROXY={whl.name}",
"--file", "release/docker/Dockerfile",
"."
])
@ -328,101 +312,100 @@ def build_docker_image(be: BuildEnviron): # pragma: no cover
assert "Mitmproxy: " in r.stdout.decode()
def build_pyinstaller(be: BuildEnviron): # pragma: no cover
def build_pyinstaller(be: BuildEnviron) -> None: # pragma: no cover
click.echo("Building pyinstaller package...")
PYINSTALLER_SPEC = os.path.join(be.release_dir, "specs")
# PyInstaller 3.2 does not bundle pydivert's Windivert binaries
PYINSTALLER_HOOKS = os.path.abspath(os.path.join(be.release_dir, "hooks"))
PYINSTALLER_TEMP = os.path.abspath(os.path.join(be.build_dir, "pyinstaller"))
PYINSTALLER_DIST = os.path.abspath(os.path.join(be.build_dir, "binaries", be.platform_tag))
PYINSTALLER_SPEC = be.release_dir / "specs"
PYINSTALLER_HOOKS = be.release_dir / "hooks"
PYINSTALLER_TEMP = be.build_dir / "pyinstaller"
PYINSTALLER_DIST = be.build_dir / "binaries" / be.platform_tag
# https://virtualenv.pypa.io/en/latest/userguide.html#windows-notes
# scripts and executables on Windows go in ENV\Scripts\ instead of ENV/bin/
if platform.system() == "Windows":
PYINSTALLER_ARGS = [
# PyInstaller < 3.2 does not handle Python 3.5's ucrt correctly.
"-p", r"C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x86",
]
else:
PYINSTALLER_ARGS = []
if os.path.exists(PYINSTALLER_TEMP):
if PYINSTALLER_TEMP.exists():
shutil.rmtree(PYINSTALLER_TEMP)
if os.path.exists(PYINSTALLER_DIST):
if PYINSTALLER_DIST.exists():
shutil.rmtree(PYINSTALLER_DIST)
for bdist, tools in sorted(be.bdists.items()):
with be.archive(os.path.join(be.dist_dir, be.archive_name(bdist))) as archive:
for tool in tools:
# We can't have a folder and a file with the same name.
if tool == "mitmproxy":
tool = "mitmproxy_main"
# This is PyInstaller, so it messes up paths.
# We need to make sure that we are in the spec folder.
with chdir(PYINSTALLER_SPEC):
click.echo("Building PyInstaller %s binary..." % tool)
excludes = []
if tool != "mitmweb":
excludes.append("mitmproxy.tools.web")
if tool != "mitmproxy_main":
excludes.append("mitmproxy.tools.console")
subprocess.check_call(
[
"pyinstaller",
"--clean",
"--workpath", PYINSTALLER_TEMP,
"--distpath", PYINSTALLER_DIST,
"--additional-hooks-dir", PYINSTALLER_HOOKS,
"--onefile",
"--console",
"--icon", "icon.ico",
# This is PyInstaller, so setting a
# different log level obviously breaks it :-)
# "--log-level", "WARN",
]
+ [x for e in excludes for x in ["--exclude-module", e]]
+ PYINSTALLER_ARGS
+ [tool]
)
# Delete the spec file - we're good without.
os.remove(f"{tool}.spec")
# Test if it works at all O:-)
executable = os.path.join(PYINSTALLER_DIST, tool)
if platform.system() == "Windows":
executable += ".exe"
# Remove _main suffix from mitmproxy executable
if "_main" in executable:
shutil.move(
executable,
executable.replace("_main", "")
)
executable = executable.replace("_main", "")
click.echo("> %s --version" % executable)
if be.platform_tag == "windows":
with chdir(PYINSTALLER_SPEC):
click.echo("Building PyInstaller binaries in directory mode...")
subprocess.check_call(
[
"pyinstaller",
"--clean",
"--workpath", PYINSTALLER_TEMP,
"--distpath", PYINSTALLER_DIST,
"./windows-dir.spec"
]
)
for tool in ["mitmproxy", "mitmdump", "mitmweb"]:
click.echo(f"> {tool} --version")
executable = (PYINSTALLER_DIST / "onedir" / tool).with_suffix(".exe")
click.echo(subprocess.check_output([executable, "--version"]).decode())
archive.add(executable, os.path.basename(executable))
click.echo("Packed {}.".format(be.archive_name(bdist)))
with be.archive(be.archive_path) as archive:
for tool in ["mitmproxy", "mitmdump", "mitmweb"]:
# We can't have a folder and a file with the same name.
if tool == "mitmproxy":
tool = "mitmproxy_main"
# Make sure that we are in the spec folder.
with chdir(PYINSTALLER_SPEC):
click.echo(f"Building PyInstaller {tool} binary...")
excludes = []
if tool != "mitmweb":
excludes.append("mitmproxy.tools.web")
if tool != "mitmproxy_main":
excludes.append("mitmproxy.tools.console")
subprocess.check_call(
[ # type: ignore
"pyinstaller",
"--clean",
"--workpath", PYINSTALLER_TEMP,
"--distpath", PYINSTALLER_DIST,
"--additional-hooks-dir", PYINSTALLER_HOOKS,
"--onefile",
"--console",
"--icon", "icon.ico",
]
+ [x for e in excludes for x in ["--exclude-module", e]]
+ [tool]
)
# Delete the spec file - we're good without.
os.remove(f"{tool}.spec")
executable = PYINSTALLER_DIST / tool
if be.platform_tag == "windows":
executable = executable.with_suffix(".exe")
# Remove _main suffix from mitmproxy executable
if "_main" in executable.name:
executable = executable.rename(
executable.with_name(executable.name.replace("_main", ""))
)
# Test if it works at all O:-)
click.echo(f"> {executable} --version")
click.echo(subprocess.check_output([executable, "--version"]).decode())
archive.add(str(executable), str(executable.name))
click.echo("Packed {}.".format(be.archive_path.name))
def build_wininstaller(be: BuildEnviron): # pragma: no cover
if not be.build_key:
click.echo("Cannot build windows installer without secret key.")
return
def build_wininstaller(be: BuildEnviron) -> None: # pragma: no cover
click.echo("Building wininstaller package...")
IB_VERSION = "20.9.0"
IB_DIR = pathlib.Path(be.release_dir) / "installbuilder"
IB_VERSION = "20.12.0"
IB_DIR = be.release_dir / "installbuilder"
IB_SETUP = IB_DIR / "setup" / f"{IB_VERSION}-installer.exe"
IB_CLI = fr"C:\Program Files (x86)\VMware InstallBuilder Enterprise {IB_VERSION}\bin\builder-cli.exe"
IB_CLI = Path(fr"C:\Program Files\VMware InstallBuilder Enterprise {IB_VERSION}\bin\builder-cli.exe")
IB_LICENSE = IB_DIR / "license.xml"
if not os.path.isfile(IB_CLI):
if not os.path.isfile(IB_SETUP):
if not IB_LICENSE.exists() and not be.build_key:
click.echo("Cannot build windows installer without secret key.")
return
if not IB_CLI.exists():
if not IB_SETUP.exists():
click.echo("Downloading InstallBuilder...")
def report(block, blocksize, total):
@ -430,22 +413,25 @@ def build_wininstaller(be: BuildEnviron): # pragma: no cover
if round(100 * done / total) != round(100 * (done - blocksize) / total):
click.secho(f"Downloading... {round(100 * done / total)}%")
tmp = IB_SETUP.with_suffix(".tmp")
urllib.request.urlretrieve(
f"https://clients.bitrock.com/installbuilder/installbuilder-enterprise-{IB_VERSION}-windows-installer.exe",
IB_SETUP.with_suffix(".tmp"),
f"https://clients.bitrock.com/installbuilder/installbuilder-enterprise-{IB_VERSION}-windows-x64-installer.exe",
tmp,
reporthook=report
)
shutil.move(str(IB_SETUP.with_suffix(".tmp")), str(IB_SETUP))
tmp.rename(IB_SETUP)
click.echo("Install InstallBuilder...")
subprocess.run([str(IB_SETUP), "--mode", "unattended", "--unattendedmodeui", "none"], check=True)
assert os.path.isfile(IB_CLI)
subprocess.run([IB_SETUP, "--mode", "unattended", "--unattendedmodeui", "none"], check=True)
assert IB_CLI.is_file()
click.echo("Decrypt InstallBuilder license...")
f = cryptography.fernet.Fernet(be.build_key.encode())
with open(IB_LICENSE.with_suffix(".xml.enc"), "rb") as infile, \
open(IB_LICENSE, "wb") as outfile:
outfile.write(f.decrypt(infile.read()))
if not IB_LICENSE.exists():
assert be.build_key
click.echo("Decrypt InstallBuilder license...")
f = cryptography.fernet.Fernet(be.build_key.encode())
with open(IB_LICENSE.with_suffix(".xml.enc"), "rb") as infile, \
open(IB_LICENSE, "wb") as outfile:
outfile.write(f.decrypt(infile.read()))
click.echo("Run InstallBuilder...")
subprocess.run([
@ -457,8 +443,7 @@ def build_wininstaller(be: BuildEnviron): # pragma: no cover
"--setvars", f"project.version={be.version}",
"--verbose"
], check=True)
assert os.path.isfile(
os.path.join(be.dist_dir, f"mitmproxy-{be.version}-windows-installer.exe"))
assert (be.dist_dir / f"mitmproxy-{be.version}-windows-installer.exe").exists()
@click.group(chain=True)
@ -507,18 +492,18 @@ def upload(): # pragma: no cover
return
if be.should_upload_aws:
num_files = len([name for name in os.listdir(be.dist_dir) if os.path.isfile(name)])
num_files = len([name for name in be.dist_dir.iterdir() if name.is_file()])
click.echo(f"Uploading {num_files} files to AWS dir {be.upload_dir}...")
subprocess.check_call([
"aws", "s3", "cp",
"--acl", "public-read",
be.dist_dir + "/",
f"{be.dist_dir}/",
f"s3://snapshots.mitmproxy.org/{be.upload_dir}/",
"--recursive",
])
if be.should_upload_pypi:
whl = glob.glob(os.path.join(be.dist_dir, 'mitmproxy-*-py3-none-any.whl'))[0]
whl, = be.dist_dir.glob('mitmproxy-*-py3-none-any.whl')
click.echo(f"Uploading {whl} to PyPi...")
subprocess.check_call(["twine", "upload", whl])

View File

@ -1,133 +1,133 @@
<project>
<shortName>mitmproxy</shortName>
<fullName>mitmproxy</fullName>
<version>1.0</version>
<leftImage>logo-installer.png</leftImage>
<logoImage>logo-installer-icon.png</logoImage>
<componentList>
<component>
<name>default</name>
<description>Default Component</description>
<canBeEdited>1</canBeEdited>
<selected>1</selected>
<show>1</show>
<folderList>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfiles</name>
<platforms>all</platforms>
<distributionFileList>
<distributionFile>
<origin>logo.ico</origin>
</distributionFile>
</distributionFileList>
</folder>
<folder>
<description>Program Files/bin</description>
<destination>${installdir}/bin</destination>
<name>binaries</name>
<platforms>all</platforms>
<distributionFileList>
<distributionFile>
<allowWildcards>1</allowWildcards>
<origin>../build/binaries/${platform_name}/*</origin>
</distributionFile>
<distributionFile>
<origin>run.ps1</origin>
</distributionFile>
</distributionFileList>
</folder>
</folderList>
<postInstallationActionList>
<addDirectoryToPath>
<insertAt>end</insertAt>
<path>${installdir}/bin</path>
<scope>user</scope>
</addDirectoryToPath>
</postInstallationActionList>
<postUninstallationActionList>
<removeDirectoryFromPath>
<path>${installdir}/bin</path>
<scope>user</scope>
</removeDirectoryFromPath>
</postUninstallationActionList>
<startMenuShortcutList>
<startMenuShortcut>
<comment>mitmproxy command line interface</comment>
<name>mitmproxy</name>
<runAsAdmin>0</runAsAdmin>
<runInTerminal>0</runInTerminal>
<windowsExec>powershell.exe</windowsExec>
<windowsExecArgs>-File "${installdir}\bin\run.ps1" mitmproxy</windowsExecArgs>
<windowsIcon>${installdir}/logo.ico</windowsIcon>
<windowsPath>${user_home_directory}</windowsPath>
</startMenuShortcut>
<startMenuShortcut>
<comment>mitmproxy web interface</comment>
<name>mitmweb</name>
<runAsAdmin>0</runAsAdmin>
<runInTerminal>0</runInTerminal>
<windowsExec>powershell.exe</windowsExec>
<windowsExecArgs>-File "${installdir}\bin\run.ps1" mitmweb</windowsExecArgs>
<windowsIcon>${installdir}/logo.ico</windowsIcon>
<windowsPath>${user_home_directory}</windowsPath>
</startMenuShortcut>
<startMenuShortcut>
<comment>mitmproxy non-interactive interface</comment>
<name>mitmdump</name>
<runAsAdmin>0</runAsAdmin>
<runInTerminal>0</runInTerminal>
<windowsExec>powershell.exe</windowsExec>
<windowsExecArgs>-File "${installdir}\bin\run.ps1" mitmdump</windowsExecArgs>
<windowsIcon>${installdir}/logo.ico</windowsIcon>
<windowsPath>${user_home_directory}</windowsPath>
</startMenuShortcut>
</startMenuShortcutList>
</component>
</componentList>
<createOsxBundleDmg>1</createOsxBundleDmg>
<disableSplashScreen>1</disableSplashScreen>
<enableRollback>1</enableRollback>
<enableTimestamp>1</enableTimestamp>
<outputDirectory>../dist</outputDirectory>
<productDisplayIcon>logo.ico</productDisplayIcon>
<saveRelativePaths>1</saveRelativePaths>
<vendor>mitmproxy.org</vendor>
<windowsExecutableIcon>logo.ico</windowsExecutableIcon>
<finalPageActionList>
<runProgram>
<program>cmd</program>
<programArguments>/c powershell -File "${installdir}\bin\run.ps1" mitmproxy &amp;</programArguments>
<progressText>Launch mitmproxy now</progressText>
<workingDirectory>${user_home_directory}</workingDirectory>
</runProgram>
</finalPageActionList>
<parameterList>
<directoryParameter>
<name>installdir</name>
<description>Installer.Parameter.installdir.description</description>
<explanation>Installer.Parameter.installdir.explanation</explanation>
<value></value>
<default>${platform_install_prefix}/${product_shortname}</default>
<allowEmptyValue>0</allowEmptyValue>
<ask>yes</ask>
<cliOptionName>prefix</cliOptionName>
<mustBeWritable>yes</mustBeWritable>
<mustExist>0</mustExist>
<width>40</width>
<postShowPageActionList>
<!-- This will skip the readytoinstall page -->
<setInstallerVariable name="next_page" value="installation"/>
</postShowPageActionList>
<preShowPageActionList>
<setInstallerVariable>
<name>ui.button(next).text</name>
<value>${msg(Installer.Button.Install)}</value>
</setInstallerVariable>
</preShowPageActionList>
</directoryParameter>
</parameterList>
</project>
<project>
<shortName>mitmproxy</shortName>
<fullName>mitmproxy</fullName>
<version>1.0</version>
<leftImage>logo-installer.png</leftImage>
<logoImage>logo-installer-icon.png</logoImage>
<componentList>
<component>
<name>default</name>
<description>Default Component</description>
<canBeEdited>1</canBeEdited>
<selected>1</selected>
<show>1</show>
<folderList>
<folder>
<description>Program Files</description>
<destination>${installdir}</destination>
<name>programfiles</name>
<platforms>all</platforms>
<distributionFileList>
<distributionFile>
<origin>logo.ico</origin>
</distributionFile>
</distributionFileList>
</folder>
<folder>
<description>Program Files/bin</description>
<destination>${installdir}/bin</destination>
<name>binaries</name>
<platforms>all</platforms>
<distributionFileList>
<distributionFile>
<allowWildcards>1</allowWildcards>
<origin>../build/binaries/windows/onedir/*</origin>
</distributionFile>
<distributionFile>
<origin>run.ps1</origin>
</distributionFile>
</distributionFileList>
</folder>
</folderList>
<postInstallationActionList>
<addDirectoryToPath>
<insertAt>end</insertAt>
<path>${installdir}/bin</path>
<scope>user</scope>
</addDirectoryToPath>
</postInstallationActionList>
<postUninstallationActionList>
<removeDirectoryFromPath>
<path>${installdir}/bin</path>
<scope>user</scope>
</removeDirectoryFromPath>
</postUninstallationActionList>
<startMenuShortcutList>
<startMenuShortcut>
<comment>mitmproxy command line interface</comment>
<name>mitmproxy</name>
<runAsAdmin>0</runAsAdmin>
<runInTerminal>0</runInTerminal>
<windowsExec>powershell.exe</windowsExec>
<windowsExecArgs>-File "${installdir}\bin\run.ps1" mitmproxy</windowsExecArgs>
<windowsIcon>${installdir}/logo.ico</windowsIcon>
<windowsPath>${user_home_directory}</windowsPath>
</startMenuShortcut>
<startMenuShortcut>
<comment>mitmproxy web interface</comment>
<name>mitmweb</name>
<runAsAdmin>0</runAsAdmin>
<runInTerminal>0</runInTerminal>
<windowsExec>powershell.exe</windowsExec>
<windowsExecArgs>-File "${installdir}\bin\run.ps1" mitmweb</windowsExecArgs>
<windowsIcon>${installdir}/logo.ico</windowsIcon>
<windowsPath>${user_home_directory}</windowsPath>
</startMenuShortcut>
<startMenuShortcut>
<comment>mitmproxy non-interactive interface</comment>
<name>mitmdump</name>
<runAsAdmin>0</runAsAdmin>
<runInTerminal>0</runInTerminal>
<windowsExec>powershell.exe</windowsExec>
<windowsExecArgs>-File "${installdir}\bin\run.ps1" mitmdump</windowsExecArgs>
<windowsIcon>${installdir}/logo.ico</windowsIcon>
<windowsPath>${user_home_directory}</windowsPath>
</startMenuShortcut>
</startMenuShortcutList>
</component>
</componentList>
<createOsxBundleDmg>1</createOsxBundleDmg>
<disableSplashScreen>1</disableSplashScreen>
<enableRollback>1</enableRollback>
<enableTimestamp>1</enableTimestamp>
<outputDirectory>../dist</outputDirectory>
<productDisplayIcon>logo.ico</productDisplayIcon>
<saveRelativePaths>1</saveRelativePaths>
<vendor>mitmproxy.org</vendor>
<windowsExecutableIcon>logo.ico</windowsExecutableIcon>
<finalPageActionList>
<runProgram>
<program>cmd</program>
<programArguments>/c powershell -File "${installdir}\bin\run.ps1" mitmproxy &amp;</programArguments>
<progressText>Launch mitmproxy now</progressText>
<workingDirectory>${user_home_directory}</workingDirectory>
</runProgram>
</finalPageActionList>
<parameterList>
<directoryParameter>
<name>installdir</name>
<description>Installer.Parameter.installdir.description</description>
<explanation>Installer.Parameter.installdir.explanation</explanation>
<value></value>
<default>${platform_install_prefix}/${product_shortname}</default>
<allowEmptyValue>0</allowEmptyValue>
<ask>yes</ask>
<cliOptionName>prefix</cliOptionName>
<mustBeWritable>yes</mustBeWritable>
<mustExist>0</mustExist>
<width>40</width>
<postShowPageActionList>
<!-- This will skip the readytoinstall page -->
<setInstallerVariable name="next_page" value="installation"/>
</postShowPageActionList>
<preShowPageActionList>
<setInstallerVariable>
<name>ui.button(next).text</name>
<value>${msg(Installer.Button.Install)}</value>
</setInstallerVariable>
</preShowPageActionList>
</directoryParameter>
</parameterList>
</project>

View File

@ -1,5 +1,6 @@
if (Get-Command wt -ErrorAction SilentlyContinue) {
Start-Process wt -ArgumentList "powershell.exe","-NoExit","-Command",$args[0]
$tool = $args[0]
if (Get-Command wt -ErrorAction SilentlyContinue) {
Start-Process wt -ArgumentList "powershell.exe","-Command","& '$PSScriptRoot\$tool.exe'"
} else {
Start-Process powershell -ArgumentList "-NoExit","-Command",$args[0]
Start-Process powershell -ArgumentList "-Command","& '$PSScriptRoot\$tool.exe'"
}

3
release/specs/mitmproxy Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env python
from mitmproxy.tools.main import mitmproxy
mitmproxy()

View File

@ -0,0 +1,42 @@
from pathlib import Path
from PyInstaller.building.api import PYZ, EXE, COLLECT
from PyInstaller.building.build_main import Analysis
assert SPECPATH == "."
here = Path(r".")
tools = ["mitmproxy", "mitmdump", "mitmweb"]
analysis = Analysis(
tools,
excludes=["tcl", "tk", "tkinter"],
pathex=[str(here)],
hookspath=[str(here / ".." / "hooks")],
)
pyz = PYZ(analysis.pure, analysis.zipped_data)
executables = []
for tool in tools:
executables.append(EXE(
pyz,
# analysis.scripts has all runtime hooks and all of our tools.
# remove the other tools.
[s for s in analysis.scripts if s[0] not in tools or s[0] == tool],
[],
exclude_binaries=True,
name=tool,
console=True,
upx=False,
icon='icon.ico'
))
COLLECT(
*executables,
analysis.binaries,
analysis.zipfiles,
analysis.datas,
strip=False,
upx=False,
name="onedir"
)

View File

@ -1,10 +1,12 @@
import io
import os
from pathlib import Path
import pytest
from release import cibuild
root = Path(__file__).parent.parent.parent
def test_buildenviron_live():
be = cibuild.BuildEnviron.from_env()
@ -14,13 +16,12 @@ def test_buildenviron_live():
def test_buildenviron_common():
be = cibuild.BuildEnviron(
system="Linux",
root_dir="/foo",
github_ref="refs/heads/master",
root_dir=root,
branch="master",
)
assert be.release_dir == os.path.join(be.root_dir, "release")
assert be.dist_dir == os.path.join(be.root_dir, "release", "dist")
assert be.build_dir == os.path.join(be.root_dir, "release", "build")
assert be.is_pull_request is False
assert be.release_dir == be.root_dir / "release"
assert be.dist_dir == be.root_dir / "release" / "dist"
assert be.build_dir == be.root_dir / "release" / "build"
assert not be.has_docker_creds
cs = io.StringIO()
@ -29,7 +30,7 @@ def test_buildenviron_common():
be = cibuild.BuildEnviron(
system="Unknown",
root_dir="/foo",
root_dir=root,
)
with pytest.raises(cibuild.BuildError):
be.version
@ -37,41 +38,27 @@ def test_buildenviron_common():
be.platform_tag
def test_buildenviron_pr():
def test_buildenviron_pr(monkeypatch):
# Simulates a PR. We build everything, but don't have access to secret
# credential env variables.
be = cibuild.BuildEnviron(
github_event_name="pull_request",
should_build_wheel=True,
should_build_pyinstaller=True,
should_build_docker=True,
)
monkeypatch.setenv("GITHUB_REF", "refs/pull/42/merge")
monkeypatch.setenv("CI_BUILD_WHEEL", "1")
monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request")
be = cibuild.BuildEnviron.from_env()
assert be.branch == "pr-42"
assert be.is_pull_request
def test_ci_systems():
github = cibuild.BuildEnviron(
github_event_name="pull_request",
github_ref="refs/heads/master"
)
assert github.is_pull_request
assert github.branch == "master"
assert github.tag == ""
github2 = cibuild.BuildEnviron(
github_event_name="pull_request",
github_ref="refs/tags/qux"
)
assert github2.is_pull_request
assert github2.branch == ""
assert github2.tag == "qux"
assert be.should_build_wheel
assert not be.should_upload_pypi
def test_buildenviron_commit():
# Simulates an ordinary commit on the master branch.
be = cibuild.BuildEnviron(
github_ref="refs/heads/master",
github_event_name="push",
system="Linux",
root_dir=root,
branch="master",
is_pull_request=False,
should_build_wheel=True,
should_build_pyinstaller=True,
should_build_docker=True,
@ -92,8 +79,8 @@ def test_buildenviron_releasetag():
# Simulates a tagged release on a release branch.
be = cibuild.BuildEnviron(
system="Linux",
root_dir="/foo",
github_ref="refs/tags/v0.0.1",
root_dir=root,
tag="v0.0.1",
should_build_wheel=True,
should_build_docker=True,
should_build_pyinstaller=True,
@ -102,7 +89,7 @@ def test_buildenviron_releasetag():
docker_password="bar",
)
assert be.tag == "v0.0.1"
assert be.branch == ""
assert be.branch is None
assert be.version == "0.0.1"
assert be.upload_dir == "0.0.1"
assert be.docker_tag == "mitmproxy/mitmproxy:0.0.1"
@ -116,8 +103,8 @@ def test_buildenviron_namedtag():
# Simulates a non-release tag on a branch.
be = cibuild.BuildEnviron(
system="Linux",
root_dir="/foo",
github_ref="refs/tags/anyname",
root_dir=root,
tag="anyname",
should_build_wheel=True,
should_build_docker=True,
should_build_pyinstaller=True,
@ -126,7 +113,7 @@ def test_buildenviron_namedtag():
docker_password="bar",
)
assert be.tag == "anyname"
assert be.branch == ""
assert be.branch is None
assert be.version == "anyname"
assert be.upload_dir == "anyname"
assert be.docker_tag == "mitmproxy/mitmproxy:anyname"
@ -140,8 +127,8 @@ def test_buildenviron_dev_branch():
# Simulates a commit on a development branch on the main repo
be = cibuild.BuildEnviron(
system="Linux",
root_dir="/foo",
github_ref="refs/heads/mybranch",
root_dir=root,
branch="mybranch",
should_build_wheel=True,
should_build_docker=True,
should_build_pyinstaller=True,
@ -149,7 +136,7 @@ def test_buildenviron_dev_branch():
docker_username="foo",
docker_password="bar",
)
assert be.tag == ""
assert be.tag is None
assert be.branch == "mybranch"
assert be.version == "mybranch"
assert be.upload_dir == "branches/mybranch"
@ -162,8 +149,8 @@ def test_buildenviron_maintenance_branch():
# Simulates a commit on a release maintenance branch on the main repo
be = cibuild.BuildEnviron(
system="Linux",
root_dir="/foo",
github_ref="refs/heads/v0.x",
root_dir=root,
branch="v0.x",
should_build_wheel=True,
should_build_docker=True,
should_build_pyinstaller=True,
@ -171,7 +158,7 @@ def test_buildenviron_maintenance_branch():
docker_username="foo",
docker_password="bar",
)
assert be.tag == ""
assert be.tag is None
assert be.branch == "v0.x"
assert be.version == "v0.x"
assert be.upload_dir == "branches/v0.x"
@ -180,38 +167,32 @@ def test_buildenviron_maintenance_branch():
assert be.is_maintenance_branch
def test_buildenviron_osx(tmpdir):
def test_buildenviron_osx(tmp_path):
be = cibuild.BuildEnviron(
system="Darwin",
root_dir="/foo",
github_ref="refs/tags/v0.0.1",
root_dir=root,
tag="v0.0.1",
)
assert be.platform_tag == "osx"
assert be.bdists == {
"mitmproxy": ["mitmproxy", "mitmdump", "mitmweb"],
}
assert be.archive_name("mitmproxy") == "mitmproxy-0.0.1-osx.tar.gz"
assert be.archive_path == be.dist_dir / "mitmproxy-0.0.1-osx.tar.gz"
a = be.archive(os.path.join(tmpdir, "arch"))
assert a
a.close()
with be.archive(tmp_path / "arch"):
pass
assert (tmp_path / "arch").exists()
def test_buildenviron_windows(tmpdir):
def test_buildenviron_windows(tmp_path):
be = cibuild.BuildEnviron(
system="Windows",
root_dir="/foo",
github_ref="refs/tags/v0.0.1",
root_dir=root,
tag="v0.0.1",
)
assert be.platform_tag == "windows"
assert be.bdists == {
"mitmproxy": ["mitmproxy", "mitmdump", "mitmweb"],
}
assert be.archive_name("mitmproxy") == "mitmproxy-0.0.1-windows.zip"
assert be.archive_path == be.dist_dir / "mitmproxy-0.0.1-windows.zip"
a = be.archive(os.path.join(tmpdir, "arch"))
assert a
a.close()
with be.archive(tmp_path / "arch"):
pass
assert (tmp_path / "arch").exists()
@pytest.mark.parametrize("version, tag, ok", [
@ -229,7 +210,8 @@ def test_buildenviron_check_version(version, tag, ok, tmpdir):
be = cibuild.BuildEnviron(
root_dir=tmpdir,
github_ref=f"refs/tags/{tag}",
system="Windows",
tag=tag,
)
if ok:
be.check_version()