From 264c3080b85526f23cbea55e235c70e6342308d4 Mon Sep 17 00:00:00 2001 From: Brandon Kiser <51934408+brandonskiser@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:27:38 -0700 Subject: [PATCH 1/2] feat: update build scripts to build qchat (#2198) --- build-config/buildspec-linux.yml | 47 +++ build-config/buildspec-macos.yml | 41 +++ build-scripts/qchatbuild.py | 587 +++++++++++++++++++++++++++++++ build-scripts/qchatmain.py | 50 +++ build-scripts/util.py | 8 +- crates/chat-cli/.gitignore | 5 +- crates/chat-cli/build.rs | 44 +++ 7 files changed, 780 insertions(+), 2 deletions(-) create mode 100644 build-config/buildspec-linux.yml create mode 100644 build-config/buildspec-macos.yml create mode 100644 build-scripts/qchatbuild.py create mode 100644 build-scripts/qchatmain.py diff --git a/build-config/buildspec-linux.yml b/build-config/buildspec-linux.yml new file mode 100644 index 0000000000..0067def79d --- /dev/null +++ b/build-config/buildspec-linux.yml @@ -0,0 +1,47 @@ +version: 0.2 + +env: + shell: bash + +phases: + install: + run-as: root + commands: + - dnf update -y + - dnf install -y python cmake bash zsh unzip git jq + - dnf swap -y gnupg2-minimal gnupg2-full + pre_build: + commands: + - export HOME=/home/codebuild-user + - export PATH="$HOME/.local/bin:$PATH" + - mkdir -p "$HOME/.local/bin" + # Create fish config dir to prevent rustup from failing + - mkdir -p "$HOME/.config/fish/conf.d" + # Install cargo + - curl --retry 5 --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + - . "$HOME/.cargo/env" + - rustup toolchain install `cat rust-toolchain.toml | grep channel | cut -d '=' -f2 | tr -d ' "'` + # Install cross only if the musl env var is set and not null + - if [ ! -z "${AMAZON_Q_BUILD_MUSL:+x}" ]; then cargo install cross --git https://github.com/cross-rs/cross; fi + # Install python/node via mise (https://mise.jdx.dev/continuous-integration.html) + - curl --retry 5 --proto '=https' --tlsv1.2 -sSf https://mise.run | sh + - mise install + - eval "$(mise activate bash --shims)" + # Install python deps + - pip3 install -r build-scripts/requirements.txt + build: + commands: + - python3.11 build-scripts/qchatmain.py build + +artifacts: + discard-paths: "yes" + base-directory: "build" + files: + - ./*.tar.gz + - ./*.zip + # Hashes + - ./*.sha256 + # Signatures + - ./*.asc + - ./*.sig + diff --git a/build-config/buildspec-macos.yml b/build-config/buildspec-macos.yml new file mode 100644 index 0000000000..8f935870ac --- /dev/null +++ b/build-config/buildspec-macos.yml @@ -0,0 +1,41 @@ +version: 0.2 + +phases: + pre_build: + commands: + - whoami + - echo "$HOME" + - echo "$SHELL" + - pwd + - ls + - mkdir -p "$HOME/.local/bin" + - export PATH="$HOME/.local/bin:$PATH" + # Create fish config dir to prevent rustup from failing + - mkdir -p "$HOME/.config/fish/conf.d" + # Install cargo + - export CARGO_HOME="$HOME/.cargo" + - curl --retry 5 --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + - . "$HOME/.cargo/env" + - rustup toolchain install `cat rust-toolchain.toml | grep channel | cut -d '=' -f2 | tr -d ' "'` + # Install cross only if the musl env var is set and not null + - if [ ! -z "${AMAZON_Q_BUILD_MUSL:+x}" ]; then cargo install cross --git https://github.com/cross-rs/cross; fi + # Install python/node via mise (https://mise.jdx.dev/continuous-integration.html) + - curl --retry 5 --proto '=https' --tlsv1.2 -sSf https://mise.run | sh + - mise install + - eval "$(mise activate zsh --shims)" + # Install python deps + - python3 -m venv scripts/.env + - source build-scripts/.env/bin/activate + - pip3 install -r build-scripts/requirements.txt + build: + commands: + - python3 build-scripts/qchatmain.py build --skip-lints --skip-tests --not-release + +artifacts: + discard-paths: "yes" + base-directory: "build" + files: + - ./*.zip + # Hashes + - ./*.sha256 + diff --git a/build-scripts/qchatbuild.py b/build-scripts/qchatbuild.py new file mode 100644 index 0000000000..4a00ca900f --- /dev/null +++ b/build-scripts/qchatbuild.py @@ -0,0 +1,587 @@ +import base64 +from dataclasses import dataclass +import json +import pathlib +from functools import cache +import os +import shutil +import time +from typing import Any, Mapping, Sequence, List, Optional +from build import generate_sha +from const import APPLE_TEAM_ID, CHAT_BINARY_NAME, CHAT_PACKAGE_NAME +from util import debug, info, isDarwin, isLinux, run_cmd, run_cmd_output, warn +from rust import cargo_cmd_name, rust_env, rust_targets +from importlib import import_module + +Args = Sequence[str | os.PathLike] +Env = Mapping[str, str | os.PathLike] +Cwd = str | os.PathLike + +BUILD_DIR_RELATIVE = pathlib.Path(os.environ.get("BUILD_DIR") or "build") +BUILD_DIR = BUILD_DIR_RELATIVE.absolute() + +CD_SIGNER_REGION = "us-west-2" +SIGNING_API_BASE_URL = "https://api.signer.builder-tools.aws.dev" + + +@dataclass +class CdSigningData: + bucket_name: str + """The bucket hosting signing artifacts accessible by CD Signer.""" + apple_notarizing_secret_arn: str + """The ARN of the secret containing the Apple ID and password, used during notarization""" + signing_role_arn: str + """The ARN of the role used by CD Signer""" + + +@dataclass +class MacOSBuildOutput: + chat_path: pathlib.Path + """The path to the chat binary""" + chat_zip_path: pathlib.Path + """The path to the chat binary zipped""" + + +def run_cargo_tests(): + args = [cargo_cmd_name()] + args.extend(["test", "--locked", "--package", CHAT_PACKAGE_NAME]) + run_cmd( + args, + env={ + **os.environ, + **rust_env(release=False), + }, + ) + + +def run_clippy(): + args = [cargo_cmd_name(), "clippy", "--locked", "--package", CHAT_PACKAGE_NAME] + run_cmd( + args, + env={ + **os.environ, + **rust_env(release=False), + }, + ) + + +def build_chat_bin( + release: bool, + output_name: str | None = None, + targets: Sequence[str] = [], +): + package = CHAT_PACKAGE_NAME + + args = [cargo_cmd_name(), "build", "--locked", "--package", package] + + for target in targets: + args.extend(["--target", target]) + + if release: + args.append("--release") + target_subdir = "release" + else: + target_subdir = "debug" + + run_cmd( + args, + env={ + **os.environ, + **rust_env(release=release), + }, + ) + + # 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 get_creds(): + boto3 = import_module("boto3") + session = boto3.Session() + credentials = session.get_credentials() + creds = credentials.get_frozen_credentials() + return creds + + +def cd_signer_request(method: str, path: str, data: str | None = None): + """ + Sends a request to the CD Signer API. + """ + SigV4Auth = import_module("botocore.auth").SigV4Auth + AWSRequest = import_module("botocore.awsrequest").AWSRequest + requests = import_module("requests") + + url = f"{SIGNING_API_BASE_URL}{path}" + headers = {"Content-Type": "application/json"} + request = AWSRequest(method=method, url=url, data=data, headers=headers) + SigV4Auth(get_creds(), "signer-builder-tools", CD_SIGNER_REGION).add_auth(request) + + for i in range(1, 8): + debug(f"Sending request {method} to {url} with data: {data}") + response = requests.request(method=method, url=url, headers=dict(request.headers), data=data) + info(f"CDSigner Request ({url}): {response.status_code}") + if response.status_code == 429: + warn(f"Too many requests, backing off for {2**i} seconds") + time.sleep(2**i) + continue + return response + + raise Exception(f"Failed to request {url}") + + +def cd_signer_create_request(manifest: Any) -> str: + """ + Sends a POST request to create a new signing request. After creation, we + need to send another request to start it. + """ + response = cd_signer_request( + method="POST", + path="/signing_requests", + data=json.dumps({"manifest": manifest}), + ) + response_json = response.json() + info(f"Signing request create: {response_json}") + request_id = response_json["signingRequestId"] + return request_id + + +def cd_signer_start_request(request_id: str, source_key: str, destination_key: str, signing_data: CdSigningData): + """ + Sends a POST request to start the signing process. + """ + response_text = cd_signer_request( + method="POST", + path=f"/signing_requests/{request_id}/start", + data=json.dumps( + { + "iamRole": f"{signing_data.signing_role_arn}", + "s3Location": { + "bucket": signing_data.bucket_name, + "sourceKey": source_key, + "destinationKey": destination_key, + }, + } + ), + ).text + info(f"Signing request start: {response_text}") + + +def cd_signer_status_request(request_id: str): + response_json = cd_signer_request( + method="GET", + path=f"/signing_requests/{request_id}", + ).json() + info(f"Signing request status: {response_json}") + return response_json["signingRequest"]["status"] + + +def cd_build_signed_package(exe_path: pathlib.Path): + """ + Creates a tarball `package.tar.gz` with the following structure: + ``` + package + ├─ EXECUTABLES_TO_SIGN + | ├─ qchat + ``` + """ + # Trying a different format without manifest.yaml and placing EXECUTABLES_TO_SIGN + # at the root. + # The docs contain conflicting information, idk what to even do here + working_dir = BUILD_DIR / "package" + shutil.rmtree(working_dir, ignore_errors=True) + (BUILD_DIR / "package" / "EXECUTABLES_TO_SIGN").mkdir(parents=True) + + shutil.copy2(exe_path, working_dir / "EXECUTABLES_TO_SIGN" / exe_path.name) + exe_path.unlink() + + run_cmd(["gtar", "-czf", "artifact.gz", "EXECUTABLES_TO_SIGN"], cwd=working_dir) + run_cmd( + ["gtar", "-czf", BUILD_DIR / "package.tar.gz", "artifact.gz"], + cwd=working_dir, + ) + + return BUILD_DIR / "package.tar.gz" + + +def manifest( + identifier: str, +): + """ + Returns the manifest arguments required when creating a new CD Signer request. + """ + return { + "type": "app", + "os": "osx", + "name": "EXECUTABLES_TO_SIGN", + "outputs": [{"label": "macos", "path": "EXECUTABLES_TO_SIGN"}], + "app": { + "identifier": identifier, + "signing_requirements": { + "certificate_type": "developerIDAppDistribution", + "app_id_prefix": APPLE_TEAM_ID, + }, + }, + } + + +def sign_executable(signing_data: CdSigningData, exe_path: pathlib.Path) -> pathlib.Path: + """ + Signs an executable with CD Signer. + + Returns: + The path to the signed executable + """ + name = exe_path.name + info(f"Signing {name}") + + info("Packaging...") + package_path = cd_build_signed_package(exe_path) + + info("Uploading...") + run_cmd(["aws", "s3", "rm", "--recursive", f"s3://{signing_data.bucket_name}/signed"]) + run_cmd(["aws", "s3", "rm", "--recursive", f"s3://{signing_data.bucket_name}/pre-signed"]) + run_cmd(["aws", "s3", "cp", package_path, f"s3://{signing_data.bucket_name}/pre-signed/package.tar.gz"]) + + info("Sending request...") + request_id = cd_signer_create_request(manifest("com.amazon.codewhisperer")) + cd_signer_start_request( + request_id=request_id, + source_key="pre-signed/package.tar.gz", + destination_key="signed/signed.zip", + signing_data=signing_data, + ) + + max_duration = 180 + end_time = time.time() + max_duration + i = 1 + while True: + info(f"Checking for signed package attempt #{i}") + status = cd_signer_status_request(request_id) + info(f"Package has status: {status}") + + match status: + case "success": + break + case "created" | "processing" | "inProgress": + pass + case "failure": + raise RuntimeError("Signing request failed") + case _: + warn(f"Unexpected status, ignoring: {status}") + + if time.time() >= end_time: + raise RuntimeError("Signed package did not appear, check signer logs") + time.sleep(2) + i += 1 + + info("Signed!") + + # CD Signer should return the signed executable in a zip file containing the structure: + # "Payload/EXECUTABLES_TO_SIGN/{executable name}". + info("Downloading...") + + # Create a new directory for unzipping the signed executable. + zip_dl_path = BUILD_DIR / pathlib.Path("signed.zip") + run_cmd(["aws", "s3", "cp", f"s3://{signing_data.bucket_name}/signed/signed.zip", zip_dl_path]) + payload_path = BUILD_DIR / "signed" + shutil.rmtree(payload_path, ignore_errors=True) + run_cmd(["unzip", zip_dl_path, "-d", payload_path]) + zip_dl_path.unlink() + signed_exe_path = BUILD_DIR / "signed" / "Payload" / "EXECUTABLES_TO_SIGN" / name + # Verify that the exe is signed + run_cmd(["codesign", "--verify", "--verbose=4", signed_exe_path]) + return signed_exe_path + + +def notarize_executable(signing_data: CdSigningData, exe_path: pathlib.Path): + """ + Submits an executable to Apple notary service. + """ + # Load the Apple id and password from secrets manager. + secret_id = signing_data.apple_notarizing_secret_arn + secret_region = parse_region_from_arn(signing_data.apple_notarizing_secret_arn) + info(f"Loading secretmanager value: {secret_id}") + secret_value = run_cmd_output( + ["aws", "--region", secret_region, "secretsmanager", "get-secret-value", "--secret-id", secret_id] + ) + secret_string = json.loads(secret_value)["SecretString"] + secrets = json.loads(secret_string) + + # Submit the exe to Apple notary service. It must be zipped first. + info(f"Submitting {exe_path} to Apple notary service") + zip_path = BUILD_DIR / f"{exe_path.name}.zip" + zip_path.unlink(missing_ok=True) + run_cmd(["zip", "-j", zip_path, exe_path], cwd=BUILD_DIR) + submit_res = run_cmd_output( + [ + "xcrun", + "notarytool", + "submit", + zip_path, + "--team-id", + APPLE_TEAM_ID, + "--apple-id", + secrets["appleId"], + "--password", + secrets["appleIdPassword"], + "--wait", + "-f", + "json", + ] + ) + debug(f"Notary service response: {submit_res}") + + # Confirm notarization succeeded. + assert json.loads(submit_res)["status"] == "Accepted" + + # Cleanup + zip_path.unlink() + + +def sign_and_notarize(signing_data: CdSigningData, chat_path: pathlib.Path) -> pathlib.Path: + """ + Signs an executable with CD Signer, and verifies it with Apple notary service. + + Returns: + The path to the signed executable. + """ + # First, sign the application + chat_path = sign_executable(signing_data, chat_path) + + # Next, notarize the application + notarize_executable(signing_data, chat_path) + + return chat_path + + +def build_macos(chat_path: pathlib.Path, signing_data: CdSigningData | None): + """ + Creates a qchat.zip under the build directory. + """ + chat_dst = BUILD_DIR / CHAT_BINARY_NAME + chat_dst.unlink(missing_ok=True) + shutil.copy2(chat_path, chat_dst) + + if signing_data: + chat_dst = sign_and_notarize(signing_data, chat_dst) + + zip_path = BUILD_DIR / f"{CHAT_BINARY_NAME}.zip" + zip_path.unlink(missing_ok=True) + + info(f"Creating zip output to {zip_path}") + run_cmd(["zip", "-j", zip_path, chat_dst], cwd=BUILD_DIR) + generate_sha(zip_path) + + +class GpgSigner: + def __init__(self, gpg_id: str, gpg_secret_key: str, gpg_passphrase: str): + self.gpg_id = gpg_id + self.gpg_secret_key = gpg_secret_key + self.gpg_passphrase = gpg_passphrase + + self.gpg_home = pathlib.Path.home() / ".gnupg-tmp" + self.gpg_home.mkdir(parents=True, exist_ok=True, mode=0o700) + + # write gpg secret key to file + self.gpg_secret_key_path = self.gpg_home / "gpg_secret" + self.gpg_secret_key_path.write_bytes(base64.b64decode(gpg_secret_key)) + + self.gpg_passphrase_path = self.gpg_home / "gpg_pass" + self.gpg_passphrase_path.write_text(gpg_passphrase) + + run_cmd(["gpg", "--version"]) + + info("Importing GPG key") + run_cmd(["gpg", "--list-keys"], env=self.gpg_env()) + run_cmd( + ["gpg", *self.sign_args(), "--allow-secret-key-import", "--import", self.gpg_secret_key_path], + env=self.gpg_env(), + ) + run_cmd(["gpg", "--list-keys"], env=self.gpg_env()) + + def gpg_env(self) -> Env: + return {**os.environ, "GNUPGHOME": self.gpg_home} + + def sign_args(self) -> Args: + return [ + "--batch", + "--pinentry-mode", + "loopback", + "--no-tty", + "--yes", + "--passphrase-file", + self.gpg_passphrase_path, + ] + + def sign_file(self, path: pathlib.Path) -> List[pathlib.Path]: + info(f"Signing {path.name}") + run_cmd( + ["gpg", "--detach-sign", *self.sign_args(), "--local-user", self.gpg_id, path], + env=self.gpg_env(), + ) + run_cmd( + ["gpg", "--detach-sign", *self.sign_args(), "--armor", "--local-user", self.gpg_id, path], + env=self.gpg_env(), + ) + return [path.with_suffix(f"{path.suffix}.asc"), path.with_suffix(f"{path.suffix}.sig")] + + def clean(self): + info("Cleaning gpg keys") + shutil.rmtree(self.gpg_home, ignore_errors=True) + + +def get_secretmanager_json(secret_id: str, secret_region: str): + info(f"Loading secretmanager value: {secret_id}") + secret_value = run_cmd_output( + ["aws", "--region", secret_region, "secretsmanager", "get-secret-value", "--secret-id", secret_id] + ) + secret_string = json.loads(secret_value)["SecretString"] + return json.loads(secret_string) + + +def load_gpg_signer() -> Optional[GpgSigner]: + if gpg_id := os.getenv("TEST_PGP_ID"): + gpg_secret_key = os.getenv("TEST_PGP_SECRET_KEY") + gpg_passphrase = os.getenv("TEST_PGP_PASSPHRASE") + if gpg_secret_key is not None and gpg_passphrase is not None: + info("Using test pgp key", gpg_id) + return GpgSigner(gpg_id=gpg_id, gpg_secret_key=gpg_secret_key, gpg_passphrase=gpg_passphrase) + + pgp_secret_arn = os.getenv("SIGNING_PGP_KEY_SECRET_ARN") + info(f"SIGNING_PGP_KEY_SECRET_ARN: {pgp_secret_arn}") + if pgp_secret_arn: + pgp_secret_region = parse_region_from_arn(pgp_secret_arn) + gpg_secret_json = get_secretmanager_json(pgp_secret_arn, pgp_secret_region) + gpg_id = gpg_secret_json["gpg_id"] + gpg_secret_key = gpg_secret_json["gpg_secret_key"] + gpg_passphrase = gpg_secret_json["gpg_passphrase"] + return GpgSigner(gpg_id=gpg_id, gpg_secret_key=gpg_secret_key, gpg_passphrase=gpg_passphrase) + else: + return None + + +def parse_region_from_arn(arn: str) -> str: + # ARN format: arn:partition:service:region:account-id:resource-type/resource-id + # Check if we have enough parts and the ARN starts with "arn:" + parts = arn.split(":") + if len(parts) >= 4: + return parts[3] + + return "" + + +def build_linux(chat_path: pathlib.Path, signer: GpgSigner | None): + """ + Creates tar.gz, tar.xz, tar.zst, and zip archives under `BUILD_DIR`. + + Each archive has the following structure: + - archive/qchat + """ + archive_name = CHAT_BINARY_NAME + + archive_path = pathlib.Path(archive_name) + archive_path.mkdir(parents=True, exist_ok=True) + shutil.copy2(chat_path, archive_path / CHAT_BINARY_NAME) + + 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}.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 build( + release: bool, + stage_name: str | None = None, + run_lints: bool = True, + run_test: bool = True, +): + BUILD_DIR.mkdir(exist_ok=True) + + disable_signing = os.environ.get("DISABLE_SIGNING") + + gpg_signer = load_gpg_signer() if not disable_signing and isLinux() else None + signing_role_arn = os.environ.get("SIGNING_ROLE_ARN") + signing_bucket_name = os.environ.get("SIGNING_BUCKET_NAME") + signing_apple_notarizing_secret_arn = os.environ.get("SIGNING_APPLE_NOTARIZING_SECRET_ARN") + if ( + not disable_signing + and isDarwin() + and signing_role_arn + and signing_bucket_name + and signing_apple_notarizing_secret_arn + ): + signing_data = CdSigningData( + bucket_name=signing_bucket_name, + apple_notarizing_secret_arn=signing_apple_notarizing_secret_arn, + signing_role_arn=signing_role_arn, + ) + else: + signing_data = None + + 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}") + + targets = rust_targets() + + info(f"Release: {release}") + info(f"Targets: {targets}") + info(f"Signing app: {signing_data is not None or gpg_signer is not None}") + + if run_test: + info("Running cargo tests") + run_cargo_tests() + + if run_lints: + info("Running cargo clippy") + run_clippy() + + info("Building", CHAT_PACKAGE_NAME) + chat_path = build_chat_bin( + release=release, + output_name=CHAT_BINARY_NAME, + targets=targets, + ) + + if isDarwin(): + build_macos(chat_path, signing_data) + else: + build_linux(chat_path, gpg_signer) diff --git a/build-scripts/qchatmain.py b/build-scripts/qchatmain.py new file mode 100644 index 0000000000..8389c5e658 --- /dev/null +++ b/build-scripts/qchatmain.py @@ -0,0 +1,50 @@ +import argparse +from qchatbuild import build + + +class StoreIfNotEmptyAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + if values and len(values) > 0: + setattr(namespace, self.dest, values) + + +parser = argparse.ArgumentParser( + prog="build", + description="Builds the qchat binary", +) +subparsers = parser.add_subparsers(help="sub-command help", dest="subparser", required=True) + +build_subparser = subparsers.add_parser(name="build") +build_subparser.add_argument( + "--stage-name", + action=StoreIfNotEmptyAction, + help="The name of the stage", +) +build_subparser.add_argument( + "--not-release", + action="store_true", + help="Build a non-release version", +) +build_subparser.add_argument( + "--skip-tests", + action="store_true", + help="Skip running npm and rust tests", +) +build_subparser.add_argument( + "--skip-lints", + action="store_true", + help="Skip running lints", +) + +args = parser.parse_args() + +match args.subparser: + case "build": + build( + release=not args.not_release, + stage_name=args.stage_name, + run_lints=not args.skip_lints, + run_test=not args.skip_tests, + ) + case _: + raise ValueError(f"Unsupported subparser {args.subparser}") diff --git a/build-scripts/util.py b/build-scripts/util.py index f2faffb1d4..39158ebdda 100644 --- a/build-scripts/util.py +++ b/build-scripts/util.py @@ -10,7 +10,7 @@ from typing import List, Mapping, Sequence from const import DESKTOP_PACKAGE_NAME, TAURI_PRODUCT_NAME - +DEBUG = "\033[94;1m" INFO = "\033[92;1m" WARN = "\033[93;1m" FAIL = "\033[91;1m" @@ -72,6 +72,10 @@ def log(*value: object, title: str, color: str | None): print(f"{color}{title}:{ENDC}", *value, flush=True) +def debug(*value: object): + log(*value, title="DEBUG", color=DEBUG) + + def info(*value: object): log(*value, title="INFO", color=INFO) @@ -100,6 +104,8 @@ def run_cmd_output( env: Env | None = None, cwd: Cwd | None = None, ) -> str: + args_str = [str(arg) for arg in args] + print(f"+ {shlex.join(args_str)}") res = subprocess.run(args, env=env, cwd=cwd, check=True, stdout=subprocess.PIPE) return res.stdout.decode("utf-8") diff --git a/crates/chat-cli/.gitignore b/crates/chat-cli/.gitignore index 0b0c025e2a..b082f8a65e 100644 --- a/crates/chat-cli/.gitignore +++ b/crates/chat-cli/.gitignore @@ -1,2 +1,5 @@ build/ -spec.ts \ No newline at end of file +spec.ts + +# This is created by the build script for macOS +src/Info.plist diff --git a/crates/chat-cli/build.rs b/crates/chat-cli/build.rs index 3d3c7c6c43..f097298af8 100644 --- a/crates/chat-cli/build.rs +++ b/crates/chat-cli/build.rs @@ -7,6 +7,10 @@ use quote::{ quote, }; +// TODO(brandonskiser): update bundle identifier for signed builds +#[cfg(target_os = "macos")] +const MACOS_BUNDLE_IDENTIFIER: &str = "com.amazon.codewhisperer"; + const DEF: &str = include_str!("./telemetry_definitions.json"); #[derive(Debug, Clone, serde::Deserialize)] @@ -39,9 +43,49 @@ struct Def { metrics: Vec, } +/// Writes a generated Info.plist for the qchat executable under src/. +/// +/// This is required for signing the executable since we must embed the Info.plist directly within +/// the binary. +#[cfg(target_os = "macos")] +fn write_plist() { + let plist = format!( + r#" + + + + CFBundlePackageType + APPL + CFBundleIdentifier + {} + CFBundleName + {} + CFBundleVersion + {} + CFBundleShortVersionString + {} + CFBundleInfoDictionaryVersion + 6.0 + NSHumanReadableCopyright + Copyright © 2022 Amazon Q CLI Team (q-cli@amazon.com):Chay Nabors (nabochay@amazon.com):Brandon Kiser (bskiser@amazon.com) All rights reserved. + + +"#, + MACOS_BUNDLE_IDENTIFIER, + option_env!("AMAZON_Q_BUILD_HASH").unwrap_or("unknown"), + option_env!("AMAZON_Q_BUILD_DATETIME").unwrap_or("unknown"), + env!("CARGO_PKG_VERSION") + ); + + std::fs::write("src/Info.plist", plist).expect("writing the Info.plist should not fail"); +} + fn main() { println!("cargo:rerun-if-changed=def.json"); + #[cfg(target_os = "macos")] + write_plist(); + let outdir = std::env::var("OUT_DIR").unwrap(); let data = serde_json::from_str::(DEF).unwrap(); From 5b4e747dd992b45da0be38038e07e63f9e693255 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 03:36:54 +0000 Subject: [PATCH 2/2] chore(deps-dev): bump globals from 16.1.0 to 16.3.0 --- updated-dependencies: - dependency-name: globals dependency-version: 16.3.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- extensions/gnome-extension/package.json | 2 +- .../gnome-legacy-extension/package.json | 2 +- extensions/vscode/package.json | 2 +- packages/dashboard-app/package.json | 2 +- pnpm-lock.yaml | 111 +++++++++--------- 5 files changed, 60 insertions(+), 59 deletions(-) diff --git a/extensions/gnome-extension/package.json b/extensions/gnome-extension/package.json index f7ee7d1ecc..ae60c32706 100644 --- a/extensions/gnome-extension/package.json +++ b/extensions/gnome-extension/package.json @@ -17,7 +17,7 @@ "devDependencies": { "@eslint/js": "^9.18.0", "eslint": "9.18.0", - "globals": "^16.1.0", + "globals": "^16.3.0", "typescript": "^5.8.3", "typescript-eslint": "^8.31.1" }, diff --git a/extensions/gnome-legacy-extension/package.json b/extensions/gnome-legacy-extension/package.json index 038e9e7918..c92796f31c 100644 --- a/extensions/gnome-legacy-extension/package.json +++ b/extensions/gnome-legacy-extension/package.json @@ -11,7 +11,7 @@ "@gi.ts/cli": "^1.5.10", "@gi.ts/lib": "^1.5.13", "eslint": "^9.18.0", - "globals": "^16.1.0" + "globals": "^16.3.0" }, "scripts": { "all": "node ./build-scripts/all.js", diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 32d3d8a201..1c1015e03a 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -49,7 +49,7 @@ "@vscode/vsce": "^2.32.0", "eslint": "^9.18.0", "glob": "^11.0.2", - "globals": "^16.1.0", + "globals": "^16.3.0", "mocha": "^11.4.0", "typescript": "^5.8.3" } diff --git a/packages/dashboard-app/package.json b/packages/dashboard-app/package.json index c81befc195..c7b47f6068 100644 --- a/packages/dashboard-app/package.json +++ b/packages/dashboard-app/package.json @@ -50,7 +50,7 @@ "eslint": "^9.18.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^16.1.0", + "globals": "^16.3.0", "postcss": "^8.5.3", "prettier": "^3.4.2", "tailwindcss": "^3.4.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8d44a9cc3..d857265a50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,8 +52,8 @@ importers: specifier: 9.18.0 version: 9.18.0(jiti@1.21.7) globals: - specifier: ^16.1.0 - version: 16.1.0 + specifier: ^16.3.0 + version: 16.3.0 typescript: specifier: ^5.8.3 version: 5.8.3 @@ -76,8 +76,8 @@ importers: specifier: ^9.18.0 version: 9.18.0(jiti@1.21.7) globals: - specifier: ^16.1.0 - version: 16.1.0 + specifier: ^16.3.0 + version: 16.3.0 extensions/vscode: devDependencies: @@ -109,8 +109,8 @@ importers: specifier: ^11.0.2 version: 11.0.2 globals: - specifier: ^16.1.0 - version: 16.1.0 + specifier: ^16.3.0 + version: 16.3.0 mocha: specifier: ^11.4.0 version: 11.4.0 @@ -598,8 +598,8 @@ importers: specifier: ^0.4.20 version: 0.4.20(eslint@9.18.0(jiti@1.21.7)) globals: - specifier: ^16.1.0 - version: 16.1.0 + specifier: ^16.3.0 + version: 16.3.0 postcss: specifier: ^8.5.3 version: 8.5.3 @@ -1480,28 +1480,28 @@ packages: resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} engines: {node: '>=18'} - '@csstools/css-calc@2.1.3': - resolution: {integrity: sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==} + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.4 - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-color-parser@3.0.9': - resolution: {integrity: sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==} + '@csstools/css-color-parser@3.0.10': + resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.4 - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-parser-algorithms@3.0.4': - resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} engines: {node: '>=18'} peerDependencies: - '@csstools/css-tokenizer': ^3.0.3 + '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-tokenizer@3.0.3': - resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} '@esbuild/aix-ppc64@0.25.3': @@ -3230,8 +3230,8 @@ packages: engines: {node: '>=4'} hasBin: true - cssstyle@4.3.1: - resolution: {integrity: sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} csstype@3.1.3: @@ -3415,8 +3415,8 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - entities@6.0.0: - resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} environment@1.1.0: @@ -3709,8 +3709,8 @@ packages: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} - form-data@4.0.2: - resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + form-data@4.0.3: + resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} engines: {node: '>= 6'} fraction.js@4.3.7: @@ -3816,8 +3816,8 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@16.1.0: - resolution: {integrity: sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==} + globals@16.3.0: + resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} engines: {node: '>=18'} globalthis@1.0.4: @@ -5891,8 +5891,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.2: - resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -5999,10 +5999,10 @@ snapshots: '@asamuzakjp/css-color@3.2.0': dependencies: - '@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-color-parser': 3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 optional: true @@ -6900,26 +6900,26 @@ snapshots: '@csstools/color-helpers@5.0.2': optional: true - '@csstools/css-calc@2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 optional: true - '@csstools/css-color-parser@3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/color-helpers': 5.0.2 - '@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) - '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 optional: true - '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': dependencies: - '@csstools/css-tokenizer': 3.0.3 + '@csstools/css-tokenizer': 3.0.4 optional: true - '@csstools/css-tokenizer@3.0.3': + '@csstools/css-tokenizer@3.0.4': optional: true '@esbuild/aix-ppc64@0.25.3': @@ -9150,7 +9150,7 @@ snapshots: cssesc@3.0.0: {} - cssstyle@4.3.1: + cssstyle@4.6.0: dependencies: '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 @@ -9320,7 +9320,7 @@ snapshots: entities@4.5.0: {} - entities@6.0.0: + entities@6.0.1: optional: true environment@1.1.0: {} @@ -9575,7 +9575,7 @@ snapshots: eslint: 9.18.0(jiti@1.21.7) esquery: 1.6.0 find-up-simple: 1.0.1 - globals: 16.1.0 + globals: 16.3.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 jsesc: 3.1.0 @@ -9765,11 +9765,12 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 - form-data@4.0.2: + form-data@4.0.3: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 + hasown: 2.0.2 mime-types: 2.1.35 optional: true @@ -9897,7 +9898,7 @@ snapshots: globals@14.0.0: {} - globals@16.1.0: {} + globals@16.3.0: {} globalthis@1.0.4: dependencies: @@ -10277,10 +10278,10 @@ snapshots: jsdom@24.1.0: dependencies: - cssstyle: 4.3.1 + cssstyle: 4.6.0 data-urls: 5.0.0 decimal.js: 10.5.0 - form-data: 4.0.2 + form-data: 4.0.3 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -10296,7 +10297,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.18.2 + ws: 8.18.3 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -11017,7 +11018,7 @@ snapshots: parse5@7.3.0: dependencies: - entities: 6.0.0 + entities: 6.0.1 optional: true password-prompt@1.1.3: @@ -12325,7 +12326,7 @@ snapshots: wrappy@1.0.2: {} - ws@8.18.2: + ws@8.18.3: optional: true xml-name-validator@5.0.0: