build-scripts/build.py (694 lines of code) (raw):
from dataclasses import dataclass
from functools import cache
import os
import json
import datetime
import pathlib
import shutil
from typing import Dict, List, Mapping, Sequence
from util import (
Package,
Variant,
enum_encoder,
get_variants,
isDarwin,
isLinux,
run_cmd,
run_cmd_output,
info,
set_executable,
version,
tauri_product_name,
)
from rust import build_hash, build_datetime, cargo_cmd_name, rust_targets, rust_env, get_target_triple
from test import run_cargo_tests, run_clippy
from signing import (
CdSigningData,
CdSigningType,
load_gpg_signer,
rebundle_dmg,
cd_sign_file,
apple_notarize_file,
)
from importlib import import_module
from const import (
APP_NAME,
CLI_BINARY_NAME,
CLI_PACKAGE_NAME,
DESKTOP_BINARY_NAME,
DESKTOP_PACKAGE_NAME,
DESKTOP_PACKAGE_PATH,
DMG_NAME,
LINUX_ARCHIVE_NAME,
LINUX_LEGACY_GNOME_EXTENSION_UUID,
LINUX_MODERN_GNOME_EXTENSION_UUID,
LINUX_PACKAGE_NAME,
MACOS_BUNDLE_ID,
PTY_BINARY_NAME,
PTY_PACKAGE_NAME,
URL_SCHEMA,
)
BUILD_DIR_RELATIVE = pathlib.Path(os.environ.get("BUILD_DIR") or "build")
BUILD_DIR = BUILD_DIR_RELATIVE.absolute()
@dataclass
class NpmBuildOutput:
dashboard_path: pathlib.Path
autocomplete_path: pathlib.Path
vscode_path: pathlib.Path
@dataclass
class MacOSBuildOutput:
dmg_path: pathlib.Path
app_gztar_path: pathlib.Path
def build_npm_packages(run_test: bool = True) -> NpmBuildOutput:
run_cmd(["pnpm", "install", "--frozen-lockfile"])
# set the version of extensions/vscode
package_json_path = pathlib.Path("extensions/vscode/package.json")
package_json_text = package_json_path.read_text()
package_json = json.loads(package_json_text)
package_json["version"] = version()
package_json_path.write_text(json.dumps(package_json, indent=2))
run_cmd(["pnpm", "build"])
if run_test:
run_cmd(["pnpm", "test", "--", "--run"])
# revert the package.json
package_json_path.write_text(package_json_text)
# copy to output
dashboard_path = BUILD_DIR / "dashboard"
shutil.rmtree(dashboard_path, ignore_errors=True)
shutil.copytree("packages/dashboard-app/dist", dashboard_path)
autocomplete_path = BUILD_DIR / "autocomplete"
shutil.rmtree(autocomplete_path, ignore_errors=True)
shutil.copytree("packages/autocomplete-app/dist", autocomplete_path)
vscode_path = BUILD_DIR / "vscode-plugin.vsix"
shutil.rmtree(vscode_path, ignore_errors=True)
shutil.copy2(f"extensions/vscode/codewhisperer-for-command-line-companion-{version()}.vsix", vscode_path)
shutil.copy2(
f"extensions/vscode/codewhisperer-for-command-line-companion-{version()}.vsix",
"crates/fig_integrations/src/vscode/vscode-plugin.vsix",
)
return NpmBuildOutput(dashboard_path=dashboard_path, autocomplete_path=autocomplete_path, vscode_path=vscode_path)
def build_cargo_bin(
variant: Variant,
release: bool,
package: str,
output_name: str | None = None,
features: Mapping[str, Sequence[str]] | None = None,
targets: Sequence[str] = [],
) -> pathlib.Path:
args = [cargo_cmd_name(), "build", "--locked", "--package", package]
if release:
args.append("--release")
for target in targets:
args.extend(["--target", target])
if features and features.get(package):
args.extend(["--features", ",".join(features[package])])
run_cmd(
args,
env={
**os.environ,
**rust_env(release=release, variant=variant),
},
)
if release:
target_subdir = "release"
else:
target_subdir = "debug"
# create "universal" binary for macos
if isDarwin():
out_path = BUILD_DIR / f"{output_name or package}-universal-apple-darwin"
args = [
"lipo",
"-create",
"-output",
out_path,
]
for target in targets:
args.append(pathlib.Path("target") / target / target_subdir / package)
run_cmd(args)
return out_path
else:
# linux does not cross compile arch
target = targets[0]
target_path = pathlib.Path("target") / target / target_subdir / package
out_path = BUILD_DIR / "bin" / f"{(output_name or package)}-{target}"
out_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(target_path, out_path)
return out_path
@cache
def gen_manifest() -> str:
return json.dumps(
{
"managed_by": "dmg",
"packaged_at": datetime.datetime.now().isoformat(),
"packaged_by": "amazon",
"variant": "full",
"version": version(),
"kind": "dmg",
"default_channel": "stable",
}
)
def build_macos_ime(
release: bool,
is_prod: bool,
signing_data: CdSigningData | None,
targets: Sequence[str] = [],
) -> pathlib.Path:
fig_input_method_bin = build_cargo_bin(
release=release, variant=Variant.FULL, package="fig_input_method", targets=targets
)
input_method_app = pathlib.Path("build/CodeWhispererInputMethod.app")
(input_method_app / "Contents/MacOS").mkdir(parents=True, exist_ok=True)
shutil.copy2(
fig_input_method_bin,
input_method_app / "Contents/MacOS/fig_input_method",
)
shutil.copy2(
"crates/fig_input_method/Info.plist",
input_method_app / "Contents/Info.plist",
)
shutil.copytree(
"crates/fig_input_method/resources",
input_method_app / "Contents/Resources",
dirs_exist_ok=True,
)
if signing_data:
info("Signing macos ime")
cd_sign_file(input_method_app, CdSigningType.IME, signing_data, is_prod=is_prod)
apple_notarize_file(input_method_app, signing_data)
return input_method_app
def macos_tauri_config(cli_path: pathlib.Path, pty_path: pathlib.Path, target: str) -> str:
config = {
"tauri": {
"bundle": {
"externalBin": [
str(cli_path).removesuffix(f"-{target}"),
str(pty_path).removesuffix(f"-{target}"),
],
"resources": ["manifest.json"],
}
}
}
return json.dumps(config)
def build_macos_desktop_app(
release: bool,
pty_path: pathlib.Path,
cli_path: pathlib.Path,
npm_packages: NpmBuildOutput,
is_prod: bool,
signing_data: CdSigningData | None,
features: Mapping[str, Sequence[str]] | None = None,
targets: Sequence[str] = [],
) -> MacOSBuildOutput:
target = get_target_triple()
info("Building macos ime")
ime_app = build_macos_ime(release=release, signing_data=signing_data, targets=targets, is_prod=is_prod)
info("Writing manifest")
manifest_path = pathlib.Path(DESKTOP_PACKAGE_PATH) / "manifest.json"
manifest_path.write_text(gen_manifest())
info("Building tauri config")
tauri_config_path = pathlib.Path(DESKTOP_PACKAGE_PATH) / "build-config.json"
tauri_config_path.write_text(macos_tauri_config(cli_path=cli_path, pty_path=pty_path, target=target))
info("Building", DESKTOP_PACKAGE_NAME)
cargo_tauri_args = [
"cargo-tauri",
"build",
"--config",
"build-config.json",
"--target",
target,
]
if features and features.get(DESKTOP_PACKAGE_NAME):
cargo_tauri_args.extend(["--features", ",".join(features[DESKTOP_PACKAGE_NAME])])
run_cmd(
cargo_tauri_args,
cwd=DESKTOP_PACKAGE_PATH,
env={**os.environ, **rust_env(release=release, variant=Variant.FULL), "BUILD_DIR": BUILD_DIR},
)
# clean up
manifest_path.unlink(missing_ok=True)
tauri_config_path.unlink(missing_ok=True)
target_bundle = pathlib.Path(f"target/{target}/release/bundle/macos/q_desktop.app")
app_path = BUILD_DIR / f"{APP_NAME}.app"
shutil.rmtree(app_path, ignore_errors=True)
shutil.copytree(target_bundle, app_path)
info_plist_path = app_path / "Contents/Info.plist"
# Change the display name of the app
run_cmd(["defaults", "write", info_plist_path, "CFBundleDisplayName", APP_NAME])
run_cmd(["defaults", "write", info_plist_path, "CFBundleName", APP_NAME])
# Specifies the app is an "agent app"
run_cmd(["defaults", "write", info_plist_path, "LSUIElement", "-bool", "TRUE"])
# Add q:// association to bundle
run_cmd(
[
"plutil",
"-insert",
"CFBundleURLTypes",
"-xml",
f"""<array>
<dict>
<key>CFBundleURLName</key>
<string>{MACOS_BUNDLE_ID}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{URL_SCHEMA}</string>
</array>
</dict>
</array>
""",
info_plist_path,
]
)
info("Copying CodeWhispererInputMethod.app into bundle")
helpers_dir = app_path / "Contents/Helpers"
helpers_dir.mkdir(parents=True, exist_ok=True)
shutil.copytree(ime_app, helpers_dir.joinpath("CodeWhispererInputMethod.app"))
info("Grabbing themes")
theme_repo = BUILD_DIR / "themes"
shutil.rmtree(theme_repo, ignore_errors=True)
run_cmd(["git", "clone", "https://github.com/withfig/themes.git", theme_repo])
shutil.copytree(theme_repo / "themes", app_path / "Contents/Resources/themes")
info("Copying dashboard into bundle")
shutil.copytree(npm_packages.dashboard_path, app_path / "Contents/Resources/dashboard")
info("Copying autocomplete into bundle")
shutil.copytree(npm_packages.autocomplete_path, app_path / "Contents/Resources/autocomplete")
# Add symlinks
# os.symlink(f"./{CLI_BINARY_NAME}", app_path / "Contents/MacOS/cli")
# os.symlink(f"./{PTY_BINARY_NAME}", app_path / "Contents/MacOS/pty")
dmg_path = BUILD_DIR / f"{DMG_NAME}.dmg"
dmg_path.unlink(missing_ok=True)
dmg_resources_dir = pathlib.Path("bundle/dmg")
background_path = dmg_resources_dir / "background.png"
icon_path = dmg_resources_dir / "VolumeIcon.icns"
# we use a dynamic import here so that we dont use this dep
# on other platforms
dmgbuild = import_module("dmgbuild")
dmgbuild.build_dmg(
volume_name=APP_NAME,
filename=dmg_path,
settings={
"format": "ULFO",
"background": str(background_path),
"icon": str(icon_path),
"text_size": 12,
"icon_size": 160,
"window_rect": ((100, 100), (660, 400)),
"files": [str(app_path)],
"symlinks": {"Applications": "/Applications"},
"icon_locations": {
app_path.name: (180, 170),
"Applications": (480, 170),
},
},
)
info(f"Created dmg at {dmg_path}")
if signing_data:
sign_and_rebundle_macos(app_path=app_path, dmg_path=dmg_path, signing_data=signing_data, is_prod=is_prod)
app_gztar_path = shutil.make_archive(str(BUILD_DIR / APP_NAME), "gztar", app_path.parent, app_path.name)
info(f"Created app tar.gz at {app_gztar_path}")
return MacOSBuildOutput(dmg_path=dmg_path, app_gztar_path=pathlib.Path(app_gztar_path))
def sign_and_rebundle_macos(app_path: pathlib.Path, dmg_path: pathlib.Path, signing_data: CdSigningData, is_prod: bool):
info("Signing app and dmg")
# Sign the application
cd_sign_file(app_path, CdSigningType.APP, signing_data, is_prod=is_prod)
# Notarize the application
apple_notarize_file(app_path, signing_data)
# Rebundle the dmg file with the signed and notarized application
rebundle_dmg(app_path=app_path, dmg_path=dmg_path)
# Sign the dmg
cd_sign_file(dmg_path, CdSigningType.DMG, signing_data, is_prod=is_prod)
# Notarize the dmg
apple_notarize_file(dmg_path, signing_data)
info("Done signing!!")
def build_linux_minimal(cli_path: pathlib.Path, pty_path: pathlib.Path):
"""
Creates tar.gz, tar.xz, tar.zst, and zip archives under `BUILD_DIR`.
Each archive has the following structure:
- archive/bin/q
- archive/bin/qterm
- archive/install.sh
- archive/README
- archive/BUILD-INFO
"""
archive_name = LINUX_ARCHIVE_NAME
archive_path = pathlib.Path(archive_name)
archive_path.mkdir(parents=True, exist_ok=True)
shutil.copy2("bundle/linux/install.sh", archive_path)
shutil.copy2("bundle/linux/README", archive_path)
# write the BUILD-INFO
build_info_path = archive_path / "BUILD-INFO"
build_info_path.write_text(
"\n".join(
[
f"BUILD_DATE={build_datetime()}",
f"BUILD_HASH={build_hash()}",
f"BUILD_TARGET_TRIPLE={get_target_triple()}",
f"BUILD_VERSION={version()}",
]
)
)
archive_bin_path = archive_path / "bin"
archive_bin_path.mkdir(parents=True, exist_ok=True)
shutil.copy2(cli_path, archive_bin_path / CLI_BINARY_NAME)
shutil.copy2(pty_path, archive_bin_path / PTY_BINARY_NAME)
signer = load_gpg_signer()
info(f"Building {archive_name}.tar.gz")
tar_gz_path = BUILD_DIR / f"{archive_name}.tar.gz"
run_cmd(["tar", "-czf", tar_gz_path, archive_path])
generate_sha(tar_gz_path)
if signer:
signer.sign_file(tar_gz_path)
info(f"Building {archive_name}.tar.xz")
tar_xz_path = BUILD_DIR / f"{archive_name}.tar.xz"
run_cmd(["tar", "-cJf", tar_xz_path, archive_path])
generate_sha(tar_xz_path)
if signer:
signer.sign_file(tar_xz_path)
info(f"Building {archive_name}.tar.zst")
tar_zst_path = BUILD_DIR / f"{archive_name}.tar.zst"
run_cmd(["tar", "-I", "zstd", "-cf", tar_zst_path, archive_path], {"ZSTD_CLEVEL": "19"})
generate_sha(tar_zst_path)
if signer:
signer.sign_file(tar_zst_path)
info(f"Building {archive_name}.zip")
zip_path = BUILD_DIR / f"{archive_name}.zip"
run_cmd(["zip", "-r", zip_path, archive_path])
generate_sha(zip_path)
if signer:
signer.sign_file(zip_path)
# clean up
shutil.rmtree(archive_path)
if signer:
signer.clean()
def linux_tauri_config(
cli_path: pathlib.Path,
pty_path: pathlib.Path,
dashboard_path: pathlib.Path,
autocomplete_path: pathlib.Path,
vscode_path: pathlib.Path,
themes_path: pathlib.Path,
legacy_extension_dir_path: pathlib.Path,
modern_extension_dir_path: pathlib.Path,
bundle_metadata_path: pathlib.Path,
target: str,
) -> str:
config = {
"tauri": {
"systemTray": {"iconPath": "icons/32x32.png"},
"bundle": {
"externalBin": [
str(cli_path).removesuffix(f"-{target}"),
str(pty_path).removesuffix(f"-{target}"),
],
"targets": ["appimage"],
"icon": ["icons/128x128.png"],
"resources": {
dashboard_path.absolute().as_posix(): "dashboard",
autocomplete_path.absolute().as_posix(): "autocomplete",
vscode_path.absolute().as_posix(): "vscode",
themes_path.absolute().as_posix(): "themes",
legacy_extension_dir_path.absolute().as_posix(): LINUX_LEGACY_GNOME_EXTENSION_UUID,
modern_extension_dir_path.absolute().as_posix(): LINUX_MODERN_GNOME_EXTENSION_UUID,
bundle_metadata_path.absolute().as_posix(): "bundle-metadata",
},
},
}
}
return json.dumps(config)
def linux_desktop_entry() -> str:
return (
"[Desktop Entry]\n"
"Categories=Development;\n"
f"Exec={DESKTOP_BINARY_NAME}\n"
f"Icon={LINUX_PACKAGE_NAME}\n"
f"Name={APP_NAME}\n"
"Terminal=false\n"
"Type=Application"
)
@dataclass
class BundleMetadata:
packaged_as: Package
def make_linux_bundle_metadata(packaged_as: Package) -> pathlib.Path:
"""
Creates the bundle metadata json file under a new directory, returning the path to the directory.
"""
metadata_dir_path = BUILD_DIR / f"{packaged_as.value}-metadata"
shutil.rmtree(metadata_dir_path, ignore_errors=True)
metadata_dir_path.mkdir(parents=True)
(metadata_dir_path / "metadata.json").write_text(
json.dumps(BundleMetadata(packaged_as=packaged_as), default=enum_encoder)
)
return metadata_dir_path
@dataclass
class LinuxDebResources:
cli_path: pathlib.Path
pty_path: pathlib.Path
desktop_path: pathlib.Path
themes_path: pathlib.Path
legacy_extension_dir_path: pathlib.Path
modern_extension_dir_path: pathlib.Path
bundle_metadata_path: pathlib.Path
npm_packages: NpmBuildOutput
@dataclass
class DebBuildOutput:
deb_path: pathlib.Path
sha_path: pathlib.Path
def build_linux_deb(
resources: LinuxDebResources,
control_path: pathlib.Path,
deb_suffix: str,
release: bool,
) -> DebBuildOutput:
"""
Builds a deb using the control file given by `control_path`.
The deb will be named using the format: `f"{LINUX_PACKAGE_NAME}{deb_suffix}.deb"`
This is kept generic in the case that we require different control files per Debian distribution.
"""
info("Packaging deb bundle for control file:", control_path)
bundles_dir = BUILD_DIR / "linux-bundles"
bundle_dir = bundles_dir / deb_suffix
shutil.rmtree(bundle_dir, ignore_errors=True)
bundle_dir.mkdir(parents=True)
info("Copying binaries")
bin_path = bundle_dir / "usr/bin"
bin_path.mkdir(parents=True)
shutil.copy(resources.cli_path, bin_path / CLI_BINARY_NAME)
shutil.copy(resources.pty_path, bin_path / PTY_BINARY_NAME)
shutil.copy(resources.desktop_path, bin_path / DESKTOP_BINARY_NAME)
info("Copying /usr/share resources")
desktop_entry_path = bundle_dir / f"usr/share/applications/{LINUX_PACKAGE_NAME}.desktop"
desktop_entry_path.parent.mkdir(parents=True)
desktop_entry_path.write_text(linux_desktop_entry())
desktop_icon_path = bundle_dir / f"usr/share/icons/hicolor/128x128/packages/{LINUX_PACKAGE_NAME}.png"
desktop_icon_path.parent.mkdir(parents=True)
share_path = bundle_dir / f"usr/share/{LINUX_PACKAGE_NAME}"
share_path.mkdir(parents=True)
shutil.copy(DESKTOP_PACKAGE_PATH / "icons" / "128x128.png", desktop_icon_path)
shutil.copytree(resources.legacy_extension_dir_path, share_path / LINUX_LEGACY_GNOME_EXTENSION_UUID)
shutil.copytree(resources.modern_extension_dir_path, share_path / LINUX_MODERN_GNOME_EXTENSION_UUID)
shutil.copytree(resources.npm_packages.autocomplete_path, share_path / "autocomplete")
shutil.copytree(resources.npm_packages.dashboard_path, share_path / "dashboard")
shutil.copytree(resources.themes_path, share_path / "themes")
# TODO: Support vscode
# vscode_path = share_path / 'vscode/vscode-plugin.vsix'
# vscode_path.parent.mkdir(parents=True)
# shutil.copy(npm_packages.vscode_path, vscode_path)
def replace_text(file: pathlib.Path, old: str, new: str):
file.write_text(file.read_text().replace(old, new))
info("Creating DEBIAN structure")
debian_path = bundle_dir / "DEBIAN"
debian_path.mkdir(parents=True)
shutil.copy(control_path, bundle_dir / "DEBIAN/control")
shutil.copy("bundle/deb/control_minimal", bundle_dir / "DEBIAN/control_minimal")
shutil.copy("bundle/deb/postrm", bundle_dir / "DEBIAN/postrm")
shutil.copy("bundle/deb/prerm", bundle_dir / "DEBIAN/prerm")
replace_text(bundle_dir / "DEBIAN/control", "$VERSION", version())
replace_text(bundle_dir / "DEBIAN/control", "$APT_ARCH", "amd64")
replace_text(bundle_dir / "DEBIAN/control_minimal", "$VERSION", version())
replace_text(bundle_dir / "DEBIAN/control_minimal", "$APT_ARCH", "amd64")
set_executable(bundle_dir / "DEBIAN/postrm")
info("Running dpkg-deb build")
dpkg_deb_args = ["dpkg-deb", "--build", "--root-owner-group"]
if not release:
# Remove compression to increase build time.
dpkg_deb_args.append("-z0")
run_cmd([*dpkg_deb_args, bundle_dir], cwd=bundles_dir)
deb_path = BUILD_DIR / f"{LINUX_PACKAGE_NAME}{deb_suffix}.deb"
info("Moving built deb to", deb_path)
(bundles_dir / f"{bundle_dir}.deb").rename(deb_path)
run_cmd(["dpkg-deb", "--info", deb_path])
sha_path = generate_sha(deb_path)
return DebBuildOutput(deb_path=deb_path, sha_path=sha_path)
def build_linux_full(
release: bool,
cli_path: pathlib.Path,
pty_path: pathlib.Path,
npm_packages: NpmBuildOutput,
features: Mapping[str, Sequence[str]] | None = None,
):
target = get_target_triple()
info("Grabbing themes")
theme_repo = BUILD_DIR / "themes"
shutil.rmtree(theme_repo, ignore_errors=True)
run_cmd(["git", "clone", "https://github.com/withfig/themes.git", theme_repo])
themes_path = theme_repo / "themes"
info("Grabbing GNOME extensions")
# Creating a directory for each GNOME extension with the structure:
# - {extension_uuid}.zip <-- extension zip installable with gnome-extensions cli
# - {extension_uuid}.version.txt <-- simple text file containing the extension version within the zip
def copy_extension(extension_uuid, extension_dir_name):
extension_dir_path = BUILD_DIR / extension_uuid
shutil.rmtree(extension_dir_path, ignore_errors=True)
extension_dir_path.mkdir(parents=True)
extension_zip_path = extension_dir_path / f"{extension_uuid}.zip"
shutil.copy(
pathlib.Path(f"extensions/{extension_dir_name}/{extension_uuid}.zip"),
extension_zip_path,
)
metadata = run_cmd_output(["unzip", "-p", extension_zip_path, "metadata.json"])
extension_version = json.loads(metadata)["version"]
pathlib.Path(extension_dir_path / f"{extension_uuid}.version.txt").write_text(str(extension_version))
return extension_dir_path
legacy_extension_dir_path = copy_extension(LINUX_LEGACY_GNOME_EXTENSION_UUID, "gnome-legacy-extension")
modern_extension_dir_path = copy_extension(LINUX_MODERN_GNOME_EXTENSION_UUID, "gnome-extension")
info("Building tauri config")
tauri_config_path = DESKTOP_PACKAGE_PATH / "build-config.json"
tauri_config_path.write_text(
linux_tauri_config(
cli_path=cli_path,
pty_path=pty_path,
dashboard_path=npm_packages.dashboard_path,
autocomplete_path=npm_packages.autocomplete_path,
vscode_path=npm_packages.vscode_path,
themes_path=themes_path,
legacy_extension_dir_path=legacy_extension_dir_path,
modern_extension_dir_path=modern_extension_dir_path,
bundle_metadata_path=make_linux_bundle_metadata(Package.APPIMAGE),
target=target,
)
)
cargo_tauri_args = [
"cargo-tauri",
"build",
"--config",
"build-config.json",
"--target",
target,
]
if features and features.get(DESKTOP_PACKAGE_NAME):
cargo_tauri_args.extend(["--features", ",".join(features[DESKTOP_PACKAGE_NAME])])
if not release:
cargo_tauri_args.extend(["--debug"])
info("Building", DESKTOP_PACKAGE_NAME)
run_cmd(
cargo_tauri_args,
cwd=DESKTOP_PACKAGE_PATH,
env={**os.environ, **rust_env(release=release, variant=Variant.FULL), "BUILD_DIR": BUILD_DIR},
)
desktop_path = pathlib.Path(f'target/{target}/{"release" if release else "debug"}/{DESKTOP_BINARY_NAME}')
deb_resources = LinuxDebResources(
cli_path=cli_path,
pty_path=pty_path,
desktop_path=desktop_path,
themes_path=themes_path,
legacy_extension_dir_path=legacy_extension_dir_path,
modern_extension_dir_path=modern_extension_dir_path,
bundle_metadata_path=make_linux_bundle_metadata(Package.DEB),
npm_packages=npm_packages,
)
deb_output = build_linux_deb(
resources=deb_resources,
control_path=pathlib.Path("bundle/deb/control"),
deb_suffix="",
release=release,
)
info("Copying AppImage to build directory")
# Determine architecture suffix based on the target triple
arch_suffix = "aarch64" if "aarch64" in target else "amd64"
info(f"Using architecture suffix: {arch_suffix} for target: {target}")
bundle_name = f"{tauri_product_name()}_{version()}_{arch_suffix}"
target_subdir = "release" if release else "debug"
bundle_grandparent_path = f"target/{target}/{target_subdir}/bundle"
appimage_path = BUILD_DIR / f"{LINUX_PACKAGE_NAME}.appimage"
shutil.copy(
pathlib.Path(f"{bundle_grandparent_path}/appimage/{bundle_name}.AppImage"),
appimage_path,
)
generate_sha(appimage_path)
signer = load_gpg_signer()
if signer:
info("Signing AppImage")
signatures = signer.sign_file(appimage_path)
run_cmd(["gpg", "--verify", signatures[0], appimage_path], env=signer.gpg_env())
info("Signing deb:", deb_output.deb_path)
run_cmd(["dpkg-sig", "-k", signer.gpg_id, "-s", "builder", deb_output.deb_path], env=signer.gpg_env())
run_cmd(["dpkg-sig", "-l", deb_output.deb_path], env=signer.gpg_env())
run_cmd(["gpg", "--verify", deb_output.deb_path], env=signer.gpg_env())
deb_output.sha_path = generate_sha(
deb_output.deb_path
) # Need to regenerate the sha since the signature is embedded inside the deb
signer.clean()
def generate_sha(path: pathlib.Path) -> pathlib.Path:
if isDarwin():
shasum_output = run_cmd_output(["shasum", "-a", "256", path])
elif isLinux():
shasum_output = run_cmd_output(["sha256sum", path])
else:
raise Exception("Unsupported platform")
sha = shasum_output.split(" ")[0]
path = path.with_name(f"{path.name}.sha256")
path.write_text(sha)
info(f"Wrote sha256sum to {path}:", sha)
return path
@dataclass
class BinaryPaths:
cli_path: pathlib.Path
pty_path: pathlib.Path
BuildOutput = Dict[Variant, BinaryPaths]
def build(
release: bool,
variants: List[Variant] | None = None,
output_bucket: str | None = None,
signing_bucket: str | None = None,
aws_account_id: str | None = None,
apple_id_secret: str | None = None,
signing_role_name: str | None = None,
stage_name: str | None = None,
run_lints: bool = True,
run_test: bool = True,
) -> BuildOutput:
variants = variants or get_variants()
if signing_bucket and aws_account_id and apple_id_secret and signing_role_name:
signing_data = CdSigningData(
bucket_name=signing_bucket,
aws_account_id=aws_account_id,
notarizing_secret_id=apple_id_secret,
signing_role_name=signing_role_name,
)
else:
signing_data = None
cargo_features: Mapping[str, Sequence[str]] = {"q_cli": ["wayland"]}
match stage_name:
case "prod" | None:
info("Building for prod")
case "gamma":
info("Building for gamma")
case _:
raise ValueError(f"Unknown stage name: {stage_name}")
info(f"Release: {release}")
info(f"Cargo features: {cargo_features}")
info(f"Signing app: {signing_data is not None}")
info(f"Variants: {[variant.name for variant in variants]}")
BUILD_DIR.mkdir(parents=True, exist_ok=True)
npm_packages = None
if Variant.FULL in variants:
info("Building npm packages")
npm_packages = build_npm_packages(run_test=run_test)
targets = rust_targets()
# Mac has multiple targets, so just use the default for the platform
# for testing and linting.
cargo_test_target = None if isDarwin() else targets[0]
if run_test:
info("Running cargo tests")
run_cargo_tests(variants=variants, features=cargo_features, target=cargo_test_target)
if run_lints:
run_clippy(variants=variants, features=cargo_features, target=cargo_test_target)
build_output: BuildOutput = {}
for variant in variants:
info(f"Building variant: {variant.name}")
info("Building", CLI_PACKAGE_NAME)
cli_path = build_cargo_bin(
variant=variant,
release=release,
package=CLI_PACKAGE_NAME,
output_name=CLI_BINARY_NAME,
features=cargo_features,
targets=targets,
)
info("Building", PTY_PACKAGE_NAME)
pty_path = build_cargo_bin(
variant=variant,
release=release,
package=PTY_PACKAGE_NAME,
output_name=PTY_BINARY_NAME,
features=cargo_features,
targets=targets,
)
if isDarwin():
info(f"Building {DMG_NAME}.dmg")
if not npm_packages:
raise RuntimeError("npm packages must be built for Mac")
build_paths = build_macos_desktop_app(
release=release,
cli_path=cli_path,
pty_path=pty_path,
npm_packages=npm_packages,
signing_data=signing_data,
features=cargo_features,
targets=targets,
is_prod=stage_name == "prod" or stage_name is None,
)
sha_path = generate_sha(build_paths.dmg_path)
if output_bucket:
staging_location = f"s3://{output_bucket}/staging/"
info(f"Build complete, sending to {staging_location}")
run_cmd(["aws", "s3", "cp", build_paths.dmg_path, staging_location])
run_cmd(["aws", "s3", "cp", build_paths.app_gztar_path, staging_location])
run_cmd(["aws", "s3", "cp", sha_path, staging_location])
elif isLinux():
if variant == Variant.FULL:
if not npm_packages:
raise RuntimeError(f"npm packages must be built for variant: {variant.name}")
build_linux_full(
release=release,
cli_path=cli_path,
pty_path=pty_path,
npm_packages=npm_packages,
features=cargo_features,
)
build_output[variant] = BinaryPaths(cli_path=cli_path, pty_path=pty_path)
else:
build_linux_minimal(cli_path=cli_path, pty_path=pty_path)
build_output[variant] = BinaryPaths(cli_path=cli_path, pty_path=pty_path)
return build_output