blob: d71153af5183cb5cfe635dced29e9b699207193d [file] [log] [blame] [edit]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'dart:io' as io;
import 'dart:typed_data';
import 'package:code_assets/code_assets.dart' show OS;
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:hooks_runner/hooks_runner.dart' show Target;
import 'package:http/http.dart' as http;
/// S_IXUSR bit from POSIX sys/stat.h.
///
/// When the file is user executable, this bit should be set in its mode.
const sIxusr = 0x40;
/// Represents a folder in Dart archive.
///
/// See https://dart.dev/get-dart/archive.
class ArchiveFolder {
final String version;
final String revision;
final Channel channel;
Uri fileUri(String path, {required Stage stage}) => SdkCache.archiveUri(
channel: channel, version: 'hash/$revision', stage: stage, path: path);
ArchiveFolder(
{required this.version, required this.revision, required this.channel});
}
/// Cache for retrieving artifacts that are not shipped with the Dart SDK.
///
/// Some binaries required by DartDev are not included with the distributed
/// Dart SDK but can be downloaded from the Dart archive. This class downloads
/// requested artifacts and stores them on disk (usually in ~/.dart).
class SdkCache {
final FileSystem fs;
late final Directory directory;
final bool verbose;
final http.Client _httpClient;
final StringSink _stderr;
final io.ProcessResult Function(String) _setUserExecutable;
SdkCache(
{required String directory,
required this.verbose,
http.Client Function()? createHttpClient,
FileSystem? fs,
StringSink? stderr,
io.ProcessResult Function(String)? chmod,
http.Client? httpClient})
: _setUserExecutable = chmod ?? _defaultSetUserExecutable,
_httpClient = httpClient ?? http.Client(),
_stderr = stderr ?? io.stderr,
fs = fs ?? LocalFileSystem() {
this.directory = this.fs.directory(directory);
}
/// Determines the remote location of artifacts for the current SDK.
///
/// The stage depends on host operating system: macOS requires signed
/// executables.
///
/// Additionally, for the main channel it validates that the given revision
/// exists in the remote archive, or falls back to the latest revision if it
/// does not (this may happen when Dart SDK is built locally from a local
/// revision, or built with RBE, in which case there's no revision at all).
Future<ArchiveFolder> resolveVersion(
{required String version,
required String revision,
required String channelName,
Target? host}) async {
host ??= Target.current;
final channel = Channel.fromString(channelName);
if (channel == null) {
throw ArgumentError('Unsupported channel: "$channelName".');
}
final folderFromArgs =
ArchiveFolder(version: version, revision: revision, channel: channel);
if (channel != Channel.main) {
// Past main channel, we always assume that given version and revision
// must exist on the server.
if (revision.isEmpty) {
throw ArgumentError(
'Channel "${channel.name}" requires valid revision.');
}
return folderFromArgs;
}
// Require signed artifacts on macOS past main channel (main channel
// releases aren't signed).
final stage = host.os == OS.macOS && channel != Channel.main
? Stage.signed
: Stage.raw;
// On the main channel, on contrary, we do not trust the revision, because
// it can be empty (if Dart SDK is built with RBE), or be at the local
// commit revision, which does not exist on storage.
if (revision.isNotEmpty) {
// Check that the given revision exists.
var exists =
await _exists(folderFromArgs.fileUri('VERSION', stage: stage));
if (exists) {
return ArchiveFolder(
version: version, revision: revision, channel: channel);
} else {
if (verbose) {
_stderr.writeln('Cannot find revision $revision in an archive.');
}
}
}
// No revision or invalid revision, checking the latest version.
_stderr.writeln('Checking the latest available revision...');
(version, revision) =
await _getLatestVersion(channel: channel, stage: stage);
if (verbose) {
_stderr.writeln('Using revision $revision.');
}
return ArchiveFolder(
version: version, revision: revision, channel: channel);
}
void _ensureExecutable(File destinationFile, OS hostOS) {
if (hostOS == OS.windows) {
return;
}
final isUserExecutable = destinationFile.statSync().mode & sIxusr == sIxusr;
if (isUserExecutable) {
return;
}
final chmodResult = _setUserExecutable(destinationFile.path);
if (chmodResult.exitCode != 0) {
throw SdkCacheException(
'Cannot make ${destinationFile.path} executable, chmod failed.\n'
'exitCode: ${chmodResult.exitCode}\n'
'stderr: ${chmodResult.stderr}');
}
}
Future<String> ensureGenSnapshot(
{required ArchiveFolder archiveFolder,
required Target target,
Target? host}) {
host ??= Target.current;
// Determine a file base name.
var basename = 'gen_snapshot_${host}_$target';
if (host.os == OS.windows) {
basename = '$basename.exe';
}
return ensureArtifact(
archiveFolder: archiveFolder,
host: host,
target: target,
basename: basename,
isExecutable: true);
}
Future<String> ensureDartAotRuntime({
required ArchiveFolder archiveFolder,
required Target target,
Target? host,
}) {
host ??= Target.current;
return ensureArtifact(
archiveFolder: archiveFolder,
basename: 'dartaotruntime_$target',
target: target,
host: host,
isExecutable: false);
}
Future<String> ensureArtifact({
required ArchiveFolder archiveFolder,
required String basename,
required Target target,
required Target host,
required bool isExecutable,
}) async {
// Calculate the local path.
var localFile =
directory.childDirectory(archiveFolder.version).childFile(basename);
if (localFile.existsSync()) {
if (isExecutable) {
_ensureExecutable(localFile, host.os);
}
return localFile.path;
}
localFile.parent.createSync(recursive: true);
final uri = archiveFolder.fileUri('sdk/$basename',
stage: resolveStage(
channel: archiveFolder.channel,
isExecutable: isExecutable,
hostOS: host.os));
try {
localFile.writeAsBytesSync(await _download(uri));
} catch (_) {
_stderr.writeln('Failed to download $uri to ${localFile.path}.');
_stderr.writeln('If the problem persists, try downloading it manually.');
rethrow;
}
if (isExecutable) {
_ensureExecutable(localFile, host.os);
}
return localFile.path;
}
/// For a given channel, find the latest available revision.
Future<(String, String)> _getLatestVersion(
{required Channel channel, required Stage stage}) async {
final json = jsonDecode(utf8.decode(await _download(archiveUri(
channel: channel, stage: stage, version: 'latest', path: 'VERSION'))));
return (json['version'] as String, json['revision'] as String);
}
Future<bool> _exists(Uri uri) async {
if (verbose) {
_stderr.writeln('Checking $uri...');
}
final response = await _httpClient.head(uri);
if (response.statusCode == io.HttpStatus.ok) {
return true;
} else if (response.statusCode == io.HttpStatus.notFound) {
return false;
} else {
throw SdkCacheException('HEAD $uri failed: ${response.statusCode}');
}
}
Future<Uint8List> _download(Uri uri) async {
if (verbose) {
_stderr.writeln('Downloading $uri...');
}
final response = await _httpClient.get(uri);
if (response.statusCode != io.HttpStatus.ok) {
throw SdkCacheException('GET $uri failed: ${response.statusCode}');
}
return response.bodyBytes;
}
static Stage resolveStage(
{required Channel channel,
required bool isExecutable,
required OS hostOS}) {
if (channel == Channel.main || !isExecutable) {
return Stage.raw;
}
return hostOS == OS.macOS ? Stage.signed : Stage.raw;
}
static Uri archiveUri(
{required Channel channel,
required Stage stage,
required String version,
required String path}) =>
Uri.https('storage.googleapis.com',
'dart-archive/channels/${channel.name}/${stage.name}/$version/$path');
/// Default implementation fdor making a [path] executable.
///
/// For testability, the actual implementation can be overridden in [SdkCache]
/// constructor.
static io.ProcessResult _defaultSetUserExecutable(String path) =>
io.Process.runSync('chmod', ['u+x', path]);
}
/// Release stage.
///
/// The stage is related to an internal release process, and not all artifacts
/// are available at all stages.
///
/// E.g. signed stage contains only signed SDK and executables for macOS.
enum Stage {
raw('raw'),
signed('signed');
final String name;
const Stage(this.name);
}
/// Release channel (e.g. beta, stable).
///
/// It corresponds to release channels at https://dart.dev/get-dart/archive and
/// does not necessarily maps to Git branches.
enum Channel {
main('main'),
dev('dev'),
beta('beta'),
stable('stable');
final String name;
const Channel(this.name);
static Channel? fromString(String name) {
for (final value in Channel.values) {
if (value.name == name) {
return value;
}
}
return null;
}
}
class SdkCacheException implements Exception {
final String message;
SdkCacheException(this.message);
@override
String toString() => 'SdkCacheException: $message';
}