Skip to content

Commit e4650cd

Browse files
authored
Add basic support for Conda (#386)
1 parent cabfcd6 commit e4650cd

File tree

3 files changed

+191
-0
lines changed

3 files changed

+191
-0
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
2+
3+
namespace Microsoft.CST.OpenSource.PackageManagers
4+
{
5+
using Contracts;
6+
using Microsoft.CST.OpenSource.Helpers;
7+
using Microsoft.CST.OpenSource.Model;
8+
using Microsoft.CST.OpenSource.PackageActions;
9+
using PackageUrl;
10+
using System;
11+
using System.Collections.Generic;
12+
using System.Net.Http;
13+
using System.Threading.Tasks;
14+
15+
public class CondaProjectManager : TypedManager<IManagerPackageVersionMetadata, CondaProjectManager.CondaArtifactType>
16+
{
17+
/// <summary>
18+
/// The type of the project manager from the package-url type specifications.
19+
/// </summary>
20+
/// <seealso href="https://www.github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst"/>
21+
public const string Type = "conda";
22+
public const string DEFAULT_CONDA_ENDPOINT = "https://repo.anaconda.com/pkgs";
23+
24+
public override string ManagerType => Type;
25+
26+
private const string TarBz2FileType = "tar.bz2";
27+
private const string CondaFileType = "conda";
28+
private const string BuildQualifier = "build";
29+
private const string SubdirQualifier = "subdir";
30+
private const string TypeQualifier = "type";
31+
private const string DefaultChannel = "main";
32+
33+
public CondaProjectManager(
34+
string directory,
35+
IManagerPackageActions<IManagerPackageVersionMetadata>? actions = null,
36+
IHttpClientFactory? httpClientFactory = null)
37+
: base(actions ?? new NoOpPackageActions(), httpClientFactory ?? new DefaultHttpClientFactory(), directory)
38+
{
39+
}
40+
41+
/// <inheritdoc />
42+
public override async IAsyncEnumerable<ArtifactUri<CondaArtifactType>> GetArtifactDownloadUrisAsync(PackageURL purl, bool useCache = true)
43+
{
44+
Check.NotNull(nameof(purl), purl);
45+
if (!IsValidPackageUrlForConda(purl))
46+
{
47+
throw new ArgumentException("Invalid package URL for Conda.");
48+
}
49+
50+
string? type = purl?.Qualifiers?.GetValueOrDefault(TypeQualifier);
51+
if (type is null)
52+
{
53+
string condaFileUrl = GetPackageDownloadUrl(purl!, CondaFileType);
54+
yield return new ArtifactUri<CondaArtifactType>(CondaArtifactType.Conda, condaFileUrl);
55+
56+
string tarBz2Url = GetPackageDownloadUrl(purl!, TarBz2FileType);
57+
yield return new ArtifactUri<CondaArtifactType>(CondaArtifactType.TarBz2, tarBz2Url);
58+
}
59+
else if (type is CondaFileType)
60+
{
61+
string downloadUrl = GetPackageDownloadUrl(purl!);
62+
yield return new ArtifactUri<CondaArtifactType>(CondaArtifactType.Conda, downloadUrl);
63+
64+
}
65+
else if (type is TarBz2FileType)
66+
{
67+
string downloadUrl = GetPackageDownloadUrl(purl!);
68+
yield return new ArtifactUri<CondaArtifactType>(CondaArtifactType.TarBz2, downloadUrl);
69+
}
70+
else
71+
{
72+
throw new ArgumentException($"Package 'type' for Conda must be '{CondaFileType}' or '{TarBz2FileType}'.");
73+
}
74+
75+
}
76+
77+
/// <inheritdoc />
78+
public override IAsyncEnumerable<PackageURL> GetPackagesFromOwnerAsync(string owner, bool useCache = true) => throw new NotImplementedException();
79+
80+
/// <inheritdoc />
81+
public override async Task<bool> PackageExistsAsync(PackageURL purl, bool useCache = true)
82+
{
83+
if (!IsValidPackageUrlForConda(purl))
84+
{
85+
return false;
86+
}
87+
string downloadUrl = GetPackageDownloadUrl(purl);
88+
HttpClient httpClient = CreateHttpClient();
89+
return await CheckHttpCacheForPackage(httpClient, downloadUrl, useCache);
90+
}
91+
92+
private bool IsValidPackageUrlForConda(PackageURL purl)
93+
{
94+
string? name = purl?.Name;
95+
string? version = purl?.Version;
96+
string? build = purl?.Qualifiers?.GetValueOrDefault(BuildQualifier);
97+
string? subDir = purl?.Qualifiers?.GetValueOrDefault(SubdirQualifier);
98+
string? type = purl?.Qualifiers?.GetValueOrDefault(TypeQualifier);
99+
100+
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(version) || string.IsNullOrEmpty(build) || string.IsNullOrEmpty(subDir))
101+
{
102+
return false;
103+
}
104+
105+
if (type is not null and not (CondaFileType or TarBz2FileType))
106+
{
107+
return false;
108+
}
109+
110+
return true;
111+
}
112+
113+
private static string GetPackageDownloadUrl(PackageURL purl, string defaultType = TarBz2FileType)
114+
{
115+
// Pre-condition: the provided purl has been validated by 'IsValidPackageUrlForConda'.
116+
string name = purl.Name;
117+
string version = purl.Version;
118+
string build = purl.Qualifiers[BuildQualifier];
119+
string subDir = purl.Qualifiers[SubdirQualifier];
120+
121+
string channel = purl?.Qualifiers?.GetValueOrDefault("channel") ?? DefaultChannel;
122+
string type = purl?.Qualifiers?.GetValueOrDefault(TypeQualifier) ?? defaultType;
123+
string feedUrl = (purl.Qualifiers?.GetValueOrDefault("repository_url") ?? DEFAULT_CONDA_ENDPOINT).EnsureTrailingSlash();
124+
125+
// Check https://docs.conda.io/projects/conda-build/en/latest/concepts/package-naming-conv.html#index-4
126+
// for details on Conda naming conventions.
127+
string fileName = $"{name}-{version}-{build}.{type}";
128+
return $"{feedUrl}{channel}/{subDir}/{fileName}";
129+
}
130+
131+
public enum CondaArtifactType
132+
{
133+
Unknown = 0,
134+
TarBz2,
135+
Conda,
136+
}
137+
}
138+
}

src/Shared/PackageManagers/ProjectManagerFactory.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ public static Dictionary<string, ConstructProjectManager> GetDefaultManagers(IHt
7171
ComposerProjectManager.Type, destinationDirectory =>
7272
new ComposerProjectManager(httpClientFactory, destinationDirectory)
7373
},
74+
{
75+
CondaProjectManager.Type, destinationDirectory =>
76+
new CondaProjectManager(destinationDirectory, new NoOpPackageActions(), httpClientFactory)
77+
},
7478
{
7579
CPANProjectManager.Type, destinationDirectory =>
7680
new CPANProjectManager(httpClientFactory, destinationDirectory)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
2+
3+
4+
namespace Microsoft.CST.OpenSource.Tests.ProjectManagerTests
5+
{
6+
using Microsoft.CST.OpenSource.Model;
7+
using Microsoft.CST.OpenSource.PackageActions;
8+
using Microsoft.CST.OpenSource.PackageManagers;
9+
using Microsoft.VisualStudio.TestTools.UnitTesting;
10+
using Moq;
11+
using PackageUrl;
12+
using System.Collections.Generic;
13+
using System.Linq;
14+
using System.Net.Http;
15+
using System.Threading.Tasks;
16+
17+
[TestClass]
18+
public class CondaProjectManagerTests
19+
{
20+
private readonly Mock<CondaProjectManager> _projectManager;
21+
private readonly IHttpClientFactory _httpFactory;
22+
23+
public CondaProjectManagerTests()
24+
{
25+
Mock<IHttpClientFactory> mockFactory = new();
26+
_httpFactory = mockFactory.Object;
27+
28+
_projectManager = new Mock<CondaProjectManager>(".", new NoOpPackageActions(), _httpFactory) { CallBase = true };
29+
}
30+
31+
[DataTestMethod]
32+
[DataRow(
33+
"pkg:conda/[email protected]?build=py36_0&channel=main&subdir=linux-64&type=tar.bz2",
34+
CondaProjectManager.CondaArtifactType.TarBz2,
35+
"https://repo.anaconda.com/pkgs/main/linux-64/absl-py-0.4.1-py36_0.tar.bz2")]
36+
[DataRow(
37+
"pkg:conda/[email protected]?build=py36_0&channel=main&subdir=linux-64&type=conda",
38+
CondaProjectManager.CondaArtifactType.Conda,
39+
"https://repo.anaconda.com/pkgs/main/linux-64/absl-py-0.4.1-py36_0.conda")]
40+
public async Task GetArtifactDownloadUrisSucceeds_Async(string purlString, CondaProjectManager.CondaArtifactType artifactType, string expectedUri)
41+
{
42+
PackageURL purl = new(purlString);
43+
List<ArtifactUri<CondaProjectManager.CondaArtifactType>> uris = await _projectManager.Object.GetArtifactDownloadUrisAsync(purl).ToListAsync();
44+
45+
Assert.AreEqual(expectedUri, uris.First().Uri.AbsoluteUri);
46+
Assert.AreEqual(artifactType, uris.First().Type);
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)