func()

in cli/azd/pkg/extensions/manager.go [277:471]


func (m *Manager) Install(ctx context.Context, id string, options *FilterOptions) (*ExtensionVersion, error) {
	if options == nil {
		options = &FilterOptions{}
	}

	installed, err := m.GetInstalled(LookupOptions{Id: id})
	if err == nil && installed != nil {
		return nil, fmt.Errorf("%s %w", id, ErrExtensionInstalled)
	}

	// Step 1: Find the extension by name
	extension, err := m.GetFromRegistry(ctx, id, options)
	if err != nil {
		return nil, err
	}

	// Step 2: Determine the version to install
	var selectedVersion *ExtensionVersion

	availableVersions := []*semver.Version{}
	availableVersionMap := map[*semver.Version]*ExtensionVersion{}

	// Create a map of available versions and sort them
	// This sorts the version from lowest to highest
	for _, extensionVersion := range extension.Versions {
		version, err := semver.NewVersion(extensionVersion.Version)
		if err != nil {
			return nil, fmt.Errorf("failed to parse version: %w", err)
		}

		availableVersionMap[version] = &extensionVersion
		availableVersions = append(availableVersions, version)
	}

	sort.Sort(semver.Collection(availableVersions))

	if options.Version == "" || options.Version == "latest" {
		latestVersion := availableVersions[len(availableVersions)-1]
		selectedVersion = availableVersionMap[latestVersion]
	} else {
		// Find the best match for the version constraint
		constraint, err := semver.NewConstraint(options.Version)
		if err != nil {
			return nil, fmt.Errorf("failed to parse version constraint: %w", err)
		}

		var bestMatch *semver.Version
		for _, v := range availableVersions {
			// Find the highest version that satisfies the constraint
			if constraint.Check(v) {
				bestMatch = v
			}
		}

		if bestMatch == nil {
			return nil, fmt.Errorf(
				"no matching version found for extension: %s and constraint: %s",
				id, options.Version,
			)
		}

		selectedVersion = availableVersionMap[bestMatch]
	}

	if selectedVersion == nil {
		return nil, fmt.Errorf("no compatible version found for extension: %s", id)
	}

	// Binaries are optional as long as dependencies are provided
	// This allows for extensions that are just extension packs
	if len(selectedVersion.Artifacts) == 0 && len(selectedVersion.Dependencies) == 0 {
		return nil, fmt.Errorf("no binaries or dependencies available for this version")
	}

	// Install dependencies
	if len(selectedVersion.Dependencies) > 0 {
		for _, dependency := range selectedVersion.Dependencies {
			dependencyInstallOptions := &FilterOptions{
				Version: dependency.Version,
				Source:  options.Source,
			}
			if _, err := m.Install(ctx, dependency.Id, dependencyInstallOptions); err != nil {
				if !errors.Is(err, ErrExtensionInstalled) {
					return nil, fmt.Errorf("failed to install dependency: %w", err)
				}
			}
		}
	}

	hasArtifact := len(selectedVersion.Artifacts) > 0
	var relativeExtensionPath string
	var targetPath string

	// Install the artifacts
	if hasArtifact {
		// Step 3: Find the artifact for the current OS
		artifact, err := findArtifactForCurrentOS(selectedVersion)
		if err != nil {
			return nil, fmt.Errorf("failed to find artifact for current OS: %w", err)
		}

		// Step 4: Download the artifact to a temp location
		tempFilePath, err := m.downloadArtifact(ctx, artifact.URL)
		if err != nil {
			return nil, fmt.Errorf("failed to download artifact: %w", err)
		}

		// Clean up the temp file after all scenarios
		defer os.Remove(tempFilePath)

		// Step 5: Validate the checksum if provided
		if err := validateChecksum(tempFilePath, artifact.Checksum); err != nil {
			return nil, fmt.Errorf("checksum validation failed: %w", err)
		}

		userConfigDir, err := config.GetUserConfigDir()
		if err != nil {
			return nil, fmt.Errorf("failed to get user config directory: %w", err)
		}

		targetDir := filepath.Join(userConfigDir, "extensions", extension.Id)
		if err := os.MkdirAll(targetDir, os.ModePerm); err != nil {
			return nil, fmt.Errorf("failed to create target directory: %w", err)
		}

		// Step 6: Copy the artifact to the target directory
		// Check if artifact is a zip file, if so extract it to the target directory
		if strings.HasSuffix(tempFilePath, ".zip") {
			if err := rzip.ExtractToDirectory(tempFilePath, targetDir); err != nil {
				return nil, fmt.Errorf("failed to extract zip file: %w", err)
			}
		} else {
			targetPath = filepath.Join(targetDir, filepath.Base(tempFilePath))
			if err := copyFile(tempFilePath, targetPath); err != nil {
				return nil, fmt.Errorf("failed to copy artifact to target location: %w", err)
			}
		}

		entryPoint := selectedVersion.EntryPoint
		if platformEntryPoint, has := artifact.AdditionalMetadata["entryPoint"]; has {
			entryPoint = fmt.Sprint(platformEntryPoint)
		}
		if entryPoint == "" {
			switch runtime.GOOS {
			case "windows":
				entryPoint = fmt.Sprintf("%s.exe", extension.Id)
			default:
				entryPoint = extension.Id
			}
		}

		targetPath := filepath.Join(targetDir, entryPoint)

		// Need to set the executable permission for the binary
		// This change is specifically required for Linux but will apply consistently across all platforms
		if err := os.Chmod(targetPath, osutil.PermissionExecutableFile); err != nil {
			return nil, fmt.Errorf("failed to set executable permission: %w", err)
		}

		relativeExtensionPath, err = filepath.Rel(userConfigDir, targetPath)
		if err != nil {
			return nil, fmt.Errorf("failed to get relative path: %w", err)
		}
	}

	// Step 7: Update the user config with the installed extension
	extensions, err := m.ListInstalled()
	if err != nil {
		return nil, fmt.Errorf("failed to list installed extensions: %w", err)
	}

	extensions[id] = &Extension{
		Id:           id,
		Capabilities: selectedVersion.Capabilities,
		Namespace:    extension.Namespace,
		DisplayName:  extension.DisplayName,
		Description:  extension.Description,
		Version:      selectedVersion.Version,
		Usage:        selectedVersion.Usage,
		Path:         relativeExtensionPath,
		Source:       extension.Source,
	}

	if err := m.userConfig.Set(installedConfigKey, extensions); err != nil {
		return nil, fmt.Errorf("failed to set extensions section: %w", err)
	}

	if err := m.configManager.Save(m.userConfig); err != nil {
		return nil, fmt.Errorf("failed to save user config: %w", err)
	}

	log.Printf("Extension '%s' (version %s) installed successfully to %s\n", id, selectedVersion.Version, targetPath)

	return selectedVersion, nil
}