-
Notifications
You must be signed in to change notification settings - Fork 26
/
Copy pathRedistributableLocator.cs
225 lines (197 loc) · 9.6 KB
/
RedistributableLocator.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
using System.Formats.Tar;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using ZstdSharp;
namespace CSnakes.Runtime.Locators;
internal class RedistributableLocator(ILogger<RedistributableLocator> logger, int installerTimeout = 360) : PythonLocator
{
private const string standaloneRelease = "20250106";
private const string MutexName = @"Global\CSnakesPythonInstall-1"; // run-time name includes Python version
private static readonly Version defaultVersion = new(3, 12, 8, 0);
protected override Version Version { get; } = defaultVersion;
protected override string GetPythonExecutablePath(string folder, bool freeThreaded = false)
{
string suffix = freeThreaded ? "t" : "";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Path.Combine(folder, $"python{suffix}.exe");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return Path.Combine(folder, "bin", $"python{Version.Major}.{Version.Minor}{suffix}");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return Path.Combine(folder, "bin", $"python{Version.Major}.{Version.Minor}{suffix}");
}
throw new PlatformNotSupportedException($"Unsupported platform: '{RuntimeInformation.OSDescription}'.");
}
public override PythonLocationMetadata LocatePython()
{
string dottedVersion = $"{Version.Major}.{Version.Minor}.{Version.Build}";
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create);
var downloadPath = Path.Join(appDataPath, "CSnakes", $"python{dottedVersion}");
var installPath = Path.Join(downloadPath, "python", "install");
using var mutex = new Mutex(initiallyOwned: false, $"{MutexName}-{dottedVersion}");
try
{
if (!mutex.WaitOne(TimeSpan.FromSeconds(installerTimeout)))
throw new TimeoutException("Python installation timed out.");
if (Directory.Exists(installPath))
return LocatePythonInternal(installPath);
}
catch (AbandonedMutexException)
{
// If the mutex was abandoned, it most probably means that the other process crashed
// and didn't even get the chance to run any clean-up. Since the state of the
// download and installation is now unreliable, start by clearing up directories and
// then proceed with the installation here.
try
{
Directory.Delete(downloadPath, recursive: true);
}
catch (DirectoryNotFoundException)
{
// If the directory didn't exist, ignore it and proceed.
}
}
// Create the folder; the install path is only created at the end.
Directory.CreateDirectory(downloadPath);
try
{
// Determine binary name, see https://gregoryszorc.com/docs/python-build-standalone/main/running.html#obtaining-distributions
string platform;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
platform = RuntimeInformation.ProcessArchitecture switch
{
Architecture.X86 => "i686-pc-windows-msvc-shared-pgo-full",
Architecture.X64 => "x86_64-pc-windows-msvc-shared-pgo-full",
_ => throw new PlatformNotSupportedException($"Unsupported architecture: '{RuntimeInformation.ProcessArchitecture}'.")
};
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
platform = RuntimeInformation.ProcessArchitecture switch
{
// No such thing as i686 mac
Architecture.X64 => "x86_64-apple-darwin-pgo+lto-full",
Architecture.Arm64 => "aarch64-apple-darwin-pgo+lto-full",
_ => throw new PlatformNotSupportedException($"Unsupported architecture: '{RuntimeInformation.ProcessArchitecture}'.")
};
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
platform = RuntimeInformation.ProcessArchitecture switch
{
Architecture.X86 => "i686-unknown-linux-gnu-pgo+lto-full",
Architecture.X64 => "x86_64-unknown-linux-gnu-pgo+lto-full",
Architecture.Arm64 => "aarch64-unknown-linux-gnu-pgo+lto-full",
// .NET doesn't run on armv7 anyway.. don't try that
_ => throw new PlatformNotSupportedException($"Unsupported architecture: '{RuntimeInformation.ProcessArchitecture}'.")
};
}
else
{
throw new PlatformNotSupportedException($"Unsupported platform: '{RuntimeInformation.OSDescription}'.");
}
string downloadUrl = $"https://github.com/astral-sh/python-build-standalone/releases/download/{standaloneRelease}/cpython-{Version.Major}.{Version.Minor}.{Version.Build}+{standaloneRelease}-{platform}.tar.zst";
// Download and extract the Zstd tarball
logger.LogDebug("Downloading Python from {DownloadUrl}", downloadUrl);
string tempFilePath = DownloadFileToTempDirectoryAsync(downloadUrl).GetAwaiter().GetResult();
string tarFilePath = DecompressZstFile(tempFilePath);
ExtractTar(tarFilePath, downloadPath, logger);
logger.LogDebug("Extracted Python to {downloadPath}", downloadPath);
// Delete the tarball and temp file
File.Delete(tarFilePath);
File.Delete(tempFilePath);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to download and extract Python");
// If the install failed somewhere, delete the folder incase it's half downloaded
if (Directory.Exists(installPath))
{
Directory.Delete(installPath, true);
}
throw;
}
mutex.ReleaseMutex(); // Everything supposedly went well so release mutex
return LocatePythonInternal(installPath);
}
protected override string GetLibPythonPath(string folder, bool freeThreaded = false)
{
string suffix = freeThreaded ? "t" : "";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Path.Combine(folder, $"python{Version.Major}{Version.Minor}{suffix}.dll");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return Path.Combine(folder, "lib", $"libpython{Version.Major}.{Version.Minor}{suffix}.dylib");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return Path.Combine(folder, "lib", $"libpython{Version.Major}.so");
}
throw new PlatformNotSupportedException($"Unsupported platform: '{RuntimeInformation.OSDescription}'.");
}
private static async Task<string> DownloadFileToTempDirectoryAsync(string fileUrl)
{
using HttpClient client = new();
using HttpResponseMessage response = await client.GetAsync(fileUrl);
response.EnsureSuccessStatusCode();
string tempFilePath = Path.GetTempFileName();
using FileStream fileStream = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
await response.Content.CopyToAsync(fileStream);
return tempFilePath;
}
private static string DecompressZstFile(string zstFilePath)
{
string tarFilePath = Path.ChangeExtension(zstFilePath, ".tar");
using var inputStream = new FileStream(zstFilePath, FileMode.Open, FileAccess.Read);
using var decompressor = new DecompressionStream(inputStream);
using var outputStream = new FileStream(tarFilePath, FileMode.Create, FileAccess.Write);
decompressor.CopyTo(outputStream);
return tarFilePath;
}
private static void ExtractTar(string tarFilePath, string extractPath, ILogger logger)
{
using FileStream tarStream = File.OpenRead(tarFilePath);
using TarReader tarReader = new(tarStream);
TarEntry? entry;
List<(string, string)> symlinks = [];
while ((entry = tarReader.GetNextEntry()) is not null)
{
string entryPath = Path.Combine(extractPath, entry.Name);
if (entry.EntryType == TarEntryType.Directory)
{
Directory.CreateDirectory(entryPath);
logger.LogDebug("Creating directory: {EntryPath}", entryPath);
}
else if (entry.EntryType == TarEntryType.RegularFile)
{
Directory.CreateDirectory(Path.GetDirectoryName(entryPath)!);
entry.ExtractToFile(entryPath, true);
} else if (entry.EntryType == TarEntryType.SymbolicLink) {
// Delay the creation of symlinks until after all files have been extracted
symlinks.Add((entryPath, entry.LinkName));
} else
{
logger.LogDebug("Skipping entry: {EntryPath} ({EntryType})", entryPath, entry.EntryType);
}
}
foreach (var (path, link) in symlinks)
{
logger.LogDebug("Creating symlink: {Path} -> {Link}", path, link);
try
{
File.CreateSymbolicLink(path, link);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create symlink: {Path} -> {Link}", path, link);
}
}
}
}