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
}