diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f2fcdaca15e..142be95f8f8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -501,6 +501,13 @@ jobs: pip install --find-links ./ azure_cli-$CLI_VERSION*whl && az self-test && az --version && sleep 5 displayName: 'Test pip Install' + +# Snap Package Build +- template: scripts/release/snap/azure-pipelines-snap.yml + parameters: + multi_arch: false + publish_to_store: false + - job: TestCore displayName: Unit Test for Core timeoutInMinutes: 10 diff --git a/scripts/release/snap/azure-pipelines-snap.yml b/scripts/release/snap/azure-pipelines-snap.yml new file mode 100644 index 00000000000..2b9c4b744b6 --- /dev/null +++ b/scripts/release/snap/azure-pipelines-snap.yml @@ -0,0 +1,148 @@ +# Azure CLI Snap Build Pipeline Template +# +# Builds snap package from wheel artifacts +# +# Usage in azure-pipelines.yml: +# - template: scripts/release/snap/azure-pipelines-snap.yml +# parameters: +# multi_arch: true + +parameters: + - name: multi_arch + type: boolean + default: true + - name: publish_to_store + type: boolean + default: false + - name: release_channel + type: string + default: 'edge' + +jobs: + - job: BuildSnapPackage + displayName: Build Snap Package + dependsOn: BuildPythonWheel + pool: + name: $(ubuntu_pool) + timeoutInMinutes: 90 + + steps: + - checkout: self + fetchDepth: 1 + + - task: DownloadPipelineArtifact@1 + displayName: 'Download Metadata' + inputs: + TargetPath: '$(Build.ArtifactStagingDirectory)/metadata' + artifactName: metadata + + - task: DownloadPipelineArtifact@1 + displayName: 'Download PyPI Packages' + inputs: + TargetPath: '$(Build.ArtifactStagingDirectory)/pypi' + artifactName: pypi + + - task: Bash@3 + displayName: 'Install Snapcraft' + inputs: + targetType: 'inline' + script: | + set -e + sudo snap install snapcraft --classic + + # LXD only needed for single-arch local build + if [ "${{ parameters.multi_arch }}" != "true" ]; then + sudo snap install lxd + sudo lxd init --auto + sudo usermod -aG lxd $(whoami) + fi + + - task: Bash@3 + displayName: 'Build Snap Package' + env: + SNAPCRAFT_STORE_CREDENTIALS: $(SNAPCRAFT_STORE_CREDENTIALS) + inputs: + targetType: 'inline' + script: | + set -e + cd scripts/release/snap + chmod +x build-snap.sh + + CLI_VERSION=$(cat $(Build.ArtifactStagingDirectory)/metadata/version) + echo "Building snap for Azure CLI version: $CLI_VERSION" + + export WHEEL_DIR="$(Build.ArtifactStagingDirectory)/pypi" + export OUTPUT_DIR="$(Build.ArtifactStagingDirectory)/snap" + + if [ "${{ parameters.multi_arch }}" = "true" ]; then + ./build-snap.sh "$CLI_VERSION" --multi-arch + else + sg lxd -c "./build-snap.sh $CLI_VERSION" + fi + + - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 + displayName: 'SBOM' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/release') + inputs: + BuildDropPath: $(Build.ArtifactStagingDirectory)/snap + + - task: PublishPipelineArtifact@0 + displayName: 'Publish Artifact: snap' + inputs: + TargetPath: $(Build.ArtifactStagingDirectory)/snap + ArtifactName: snap + + - ${{ if eq(parameters.publish_to_store, true) }}: + - task: Bash@3 + displayName: 'Publish to Snap Store' + env: + SNAPCRAFT_STORE_CREDENTIALS: $(SNAPCRAFT_STORE_CREDENTIALS) + inputs: + targetType: 'inline' + script: | + cd $(Build.ArtifactStagingDirectory)/snap + for snap_file in *.snap; do + echo "Uploading $snap_file to ${{ parameters.release_channel }}..." + snapcraft upload "$snap_file" --release=${{ parameters.release_channel }} + done + + - job: TestSnapPackage + displayName: Test Snap Package + dependsOn: BuildSnapPackage + condition: succeeded() + pool: + name: $(ubuntu_pool) + + steps: + - task: DownloadPipelineArtifact@1 + displayName: 'Download Metadata' + inputs: + TargetPath: '$(Build.ArtifactStagingDirectory)/metadata' + artifactName: metadata + + - task: DownloadPipelineArtifact@1 + displayName: 'Download Snap Package' + inputs: + TargetPath: '$(Build.ArtifactStagingDirectory)/snap' + artifactName: snap + + - task: Bash@3 + displayName: 'Test Snap Installation' + inputs: + targetType: 'inline' + script: | + set -e + CLI_VERSION=$(cat $(Build.ArtifactStagingDirectory)/metadata/version) + + # Test amd64 snap (agent is amd64) + SNAP_FILE=$(Build.ArtifactStagingDirectory)/snap/azure-cli_${CLI_VERSION}_amd64.snap + + echo "Installing snap: $SNAP_FILE" + sudo snap install "$SNAP_FILE" --dangerous + + echo "Testing Azure CLI..." + azure-cli.az --version + azure-cli.az self-test + + echo "Snap info:" + snap info azure-cli \ No newline at end of file diff --git a/scripts/release/snap/build-snap.sh b/scripts/release/snap/build-snap.sh new file mode 100644 index 00000000000..3c167eb1e3e --- /dev/null +++ b/scripts/release/snap/build-snap.sh @@ -0,0 +1,234 @@ +#!/bin/bash +# Azure CLI Snap Build Script +# Build snap package from wheel files +# +# Usage: ./build-snap.sh [VERSION] [--multi-arch] +# +# Environment variables: +# WHEEL_DIR Directory containing wheel files (default: /mnt/pypi) +# OUTPUT_DIR Output directory for snap file (default: /mnt/output) + +set -e + +VERSION="" +MULTI_ARCH=false +WHEEL_DIR="${WHEEL_DIR:-/mnt/pypi}" +OUTPUT_DIR="${OUTPUT_DIR:-/mnt/output}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD_DIR="${SCRIPT_DIR}/snap-build" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --multi-arch) + MULTI_ARCH=true + shift + ;; + help|--help|-h) + echo "Azure CLI Snap Build Script" + echo "" + echo "Usage: $0 [VERSION] [--multi-arch]" + echo "" + echo "Arguments:" + echo " VERSION CLI version (auto-detected from wheel if not specified)" + echo " --multi-arch Build for amd64 and arm64 using Launchpad remote-build" + echo "" + echo "Environment Variables:" + echo " WHEEL_DIR Directory containing wheel files (default: /mnt/pypi)" + echo " OUTPUT_DIR Output directory for snap file (default: /mnt/output)" + echo "" + echo "Examples:" + echo " $0 # Build amd64 only" + echo " $0 --multi-arch # Build amd64 + arm64" + echo " $0 2.81.0 --multi-arch # Build specific version, multi-arch" + exit 0 + ;; + *) + if [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + VERSION="$1" + fi + shift + ;; + esac +done + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Get version from wheel filename +get_version() { + if [ -n "$VERSION" ]; then + echo "$VERSION" + return + fi + + local wheel_file + wheel_file=$(ls -1 "${WHEEL_DIR}"/azure_cli-*.whl 2>/dev/null | head -1) + if [ -n "$wheel_file" ]; then + basename "$wheel_file" | sed -E 's/azure_cli-([0-9]+\.[0-9]+\.[0-9]+).*/\1/' + return + fi + + log_error "Cannot detect version. Please specify: $0 2.81.0" + exit 1 +} + +# Validate wheel files exist +validate_wheels() { + log_info "Validating wheel files in ${WHEEL_DIR}..." + + if [ ! -d "$WHEEL_DIR" ]; then + log_error "Wheel directory not found: $WHEEL_DIR" + exit 1 + fi + + local wheel_count + wheel_count=$(ls -1 "${WHEEL_DIR}"/*.whl 2>/dev/null | wc -l) + if [ "$wheel_count" -eq 0 ]; then + log_error "No wheel files found in $WHEEL_DIR" + exit 1 + fi + + log_info "Found $wheel_count wheel files" +} + +# Create snapcraft.yaml from template +create_snapcraft_yaml() { + local version=$1 + + log_info "Creating snapcraft.yaml for version $version..." + + mkdir -p "$BUILD_DIR/scripts" + mkdir -p "$BUILD_DIR/wheels" + + # Copy wheel files + cp "${WHEEL_DIR}"/*.whl "$BUILD_DIR/wheels/" + + # Wrapper script + cat > "$BUILD_DIR/scripts/az-wrapper" << 'WRAPPER' +#!/bin/bash +PYTHON_VERSION="$("${SNAP}/opt/az/bin/python3" - << 'EOF' +import sys +print(f"python{sys.version_info.major}.{sys.version_info.minor}") +EOF +)" +export PYTHONPATH="${SNAP}/opt/az/lib/${PYTHON_VERSION}/site-packages" +export PATH="${SNAP}/opt/az/bin:${PATH}" +export AZ_INSTALLER="SNAP" +exec "${SNAP}/opt/az/bin/python3" -m azure.cli "$@" +WRAPPER + chmod +x "$BUILD_DIR/scripts/az-wrapper" + + # Determine architecture section + local arch_section + if [ "$MULTI_ARCH" = true ]; then + arch_section="architectures:\\ + - build-on: [amd64]\\ + build-for: [amd64]\\ + - build-on: [arm64]\\ + build-for: [arm64]" + else + arch_section="architectures:\\ + - build-on: amd64" + fi + + # Generate snapcraft.yaml from template + # Template variables: + # ${CLI_VERSION} - Azure CLI version (e.g., 2.81.0) + # ${ARCHITECTURES} - Architecture configuration block + sed -e "s/\${CLI_VERSION}/${version}/g" \ + -e "s/\${ARCHITECTURES}/${arch_section}/" \ + "${SCRIPT_DIR}/snapcraft.yaml.template" > "$BUILD_DIR/snapcraft.yaml" + + log_success "snapcraft.yaml created from template" +} + +# Build snap +build_snap() { + local version=$1 + cd "$BUILD_DIR" + + if [ "$MULTI_ARCH" = true ]; then + log_info "Building for amd64 + arm64 using Launchpad remote-build..." + log_warn "This requires SNAPCRAFT_STORE_CREDENTIALS and may take 10-20 minutes" + + if [ -z "$SNAPCRAFT_STORE_CREDENTIALS" ]; then + log_error "SNAPCRAFT_STORE_CREDENTIALS not set. Required for remote-build." + exit 1 + fi + + snapcraft remote-build --launchpad-accept-public-upload + + log_success "Multi-arch build completed!" + ls -la *.snap 2>/dev/null || true + else + log_info "Building for local architecture (amd64)..." + + # Clean old build + if [ -d "parts" ] || [ -d "stage" ] || [ -d "prime" ]; then + snapcraft clean 2>/dev/null || true + fi + + # Use --destructive-mode in CI (no LXD container, better network access) + # Use --use-lxd for local builds (isolated environment) + if [ -n "$CI" ] || [ -n "$BUILD_BUILDID" ]; then + log_info "CI environment detected, using destructive mode..." + snapcraft --destructive-mode --verbosity=verbose + else + snapcraft --use-lxd --verbosity=verbose + fi + + local snap_file + snap_file=$(ls -1 *.snap 2>/dev/null | head -1) + + if [ -n "$snap_file" ]; then + log_success "Build successful: $snap_file" + log_info "Size: $(du -h "$snap_file" | cut -f1)" + else + log_error "Build failed - no snap file generated" + exit 1 + fi + fi + + # Copy to output directory + mkdir -p "$OUTPUT_DIR" + cp *.snap "$OUTPUT_DIR/" 2>/dev/null || true + log_success "Copied snap files to: $OUTPUT_DIR" + + # Copy build logs to output directory + local log_dir="$HOME/.local/state/snapcraft/log" + if [ -d "$log_dir" ]; then + cp "$log_dir"/snapcraft-*.log "$OUTPUT_DIR/" 2>/dev/null || true + log_info "Copied build logs to: $OUTPUT_DIR" + fi +} + +# Main +main() { + log_info "Azure CLI Snap Build Script" + log_info "============================" + + validate_wheels + + VERSION=$(get_version) + log_info "CLI Version: $VERSION" + log_info "Multi-arch: $MULTI_ARCH" + log_info "Wheel Dir: $WHEEL_DIR" + log_info "Output Dir: $OUTPUT_DIR" + + create_snapcraft_yaml "$VERSION" + build_snap "$VERSION" + + log_success "Build completed successfully!" +} + +main \ No newline at end of file diff --git a/scripts/release/snap/snapcraft.yaml.template b/scripts/release/snap/snapcraft.yaml.template new file mode 100644 index 00000000000..fdc59360f32 --- /dev/null +++ b/scripts/release/snap/snapcraft.yaml.template @@ -0,0 +1,61 @@ +name: azure-cli +version: "${CLI_VERSION}" +title: Azure CLI +summary: Microsoft Azure Command-Line Interface +description: | + The Azure CLI is a command-line tool for managing Azure resources. + It provides a consistent interface across Azure services with features + including resource management, scripting support, and cross-platform + compatibility. + + Documentation: https://learn.microsoft.com/cli/azure/ + +license: MIT +contact: https://github.com/Azure/azure-cli/issues +issues: https://github.com/Azure/azure-cli/issues +source-code: https://github.com/Azure/azure-cli +website: https://learn.microsoft.com/cli/azure/ + +grade: stable +confinement: strict +base: core22 + +${ARCHITECTURES} + +apps: + az: + command: bin/az-wrapper + plugs: + - home + - network + - network-bind + - ssh-keys + - removable-media + - desktop # Allow opening URLs in browser (for az login) + - browser-support # Browser integration support + +parts: + wrapper: + plugin: dump + source: scripts/ + organize: + az-wrapper: bin/az-wrapper + + azure-cli: + plugin: python + source: wheels/ + build-packages: + - python3-dev + - libffi-dev + - libssl-dev + stage-packages: + - python3 + - python3-pip + - libffi8 + - libssl3 + override-build: | + craftctl default + ${CRAFT_PART_INSTALL}/bin/pip3 install --find-links=${CRAFT_PART_SRC} ${CRAFT_PART_SRC}/azure_cli*.whl + organize: + bin: opt/az/bin + lib: opt/az/lib