| 1 | import 'dart:ffi'; |
| 2 | import 'dart:io'; |
| 3 | import 'dart:isolate'; |
| 4 | |
| 5 | class OndeCliException implements Exception { |
| 6 | OndeCliException(this.message); |
| 7 | |
| 8 | final String message; |
| 9 | |
| 10 | @override |
| 11 | String toString() => message; |
| 12 | } |
| 13 | |
| 14 | Future<int> runOndeCli(List<String> arguments) async { |
| 15 | final executablePath = await resolveExecutablePath(); |
| 16 | await ensureExecutablePermissions(executablePath); |
| 17 | |
| 18 | final process = await Process.start( |
| 19 | executablePath, |
| 20 | arguments, |
| 21 | mode: ProcessStartMode.inheritStdio, |
| 22 | workingDirectory: Directory.current.path, |
| 23 | ); |
| 24 | |
| 25 | return process.exitCode; |
| 26 | } |
| 27 | |
| 28 | Future<String> resolveExecutablePath() async { |
| 29 | final version = await packageVersion(); |
| 30 | final cacheDirectory = await cliCacheDirectory(version); |
| 31 | final executablePath = [ |
| 32 | cacheDirectory.path, |
| 33 | runtimeIdentifier(), |
| 34 | executableName(), |
| 35 | ].join(Platform.pathSeparator); |
| 36 | |
| 37 | final executableFile = File(executablePath); |
| 38 | if (!await executableFile.exists()) { |
| 39 | await downloadExecutable(version, executableFile); |
| 40 | } |
| 41 | |
| 42 | return executablePath; |
| 43 | } |
| 44 | |
| 45 | Future<void> downloadExecutable(String version, File executableFile) async { |
| 46 | final parent = executableFile.parent; |
| 47 | if (!await parent.exists()) { |
| 48 | await parent.create(recursive: true); |
| 49 | } |
| 50 | |
| 51 | final tempFile = File('${executableFile.path}.download'); |
| 52 | if (await tempFile.exists()) { |
| 53 | await tempFile.delete(); |
| 54 | } |
| 55 | |
| 56 | final client = HttpClient(); |
| 57 | try { |
| 58 | final request = await client.getUrl(downloadUri(version)); |
| 59 | final response = await request.close(); |
| 60 | |
| 61 | if (response.statusCode != HttpStatus.ok) { |
| 62 | throw OndeCliException( |
| 63 | 'Failed to download onde ${version} for ${runtimeIdentifier()} ' |
| 64 | '(HTTP ${response.statusCode}).', |
| 65 | ); |
| 66 | } |
| 67 | |
| 68 | final sink = tempFile.openWrite(); |
| 69 | await response.pipe(sink); |
| 70 | await sink.close(); |
| 71 | |
| 72 | if (await executableFile.exists()) { |
| 73 | await executableFile.delete(); |
| 74 | } |
| 75 | |
| 76 | await tempFile.rename(executableFile.path); |
| 77 | } on OndeCliException { |
| 78 | rethrow; |
| 79 | } catch (error) { |
| 80 | throw OndeCliException('Failed to download the native onde binary: $error'); |
| 81 | } finally { |
| 82 | client.close(force: true); |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | Uri downloadUri(String version) { |
| 87 | return Uri.parse( |
| 88 | 'https://github.com/ondeinference/onde-cli/releases/download/' |
| 89 | 'v$version/${releaseAssetName()}', |
| 90 | ); |
| 91 | } |
| 92 | |
| 93 | Future<Directory> cliCacheDirectory(String version) async { |
| 94 | final home = homeDirectoryPath(); |
| 95 | return Directory( |
| 96 | [home, '.onde', 'cli', version].join(Platform.pathSeparator), |
| 97 | ); |
| 98 | } |
| 99 | |
| 100 | String homeDirectoryPath() { |
| 101 | final env = Platform.environment; |
| 102 | final home = env['HOME']; |
| 103 | if (home != null && home.isNotEmpty) { |
| 104 | return home; |
| 105 | } |
| 106 | |
| 107 | final userProfile = env['USERPROFILE']; |
| 108 | if (userProfile != null && userProfile.isNotEmpty) { |
| 109 | return userProfile; |
| 110 | } |
| 111 | |
| 112 | final homeDrive = env['HOMEDRIVE']; |
| 113 | final homePath = env['HOMEPATH']; |
| 114 | if (homeDrive != null && homePath != null) { |
| 115 | return '$homeDrive$homePath'; |
| 116 | } |
| 117 | |
| 118 | throw OndeCliException('Could not resolve the home directory for onde.'); |
| 119 | } |
| 120 | |
| 121 | Future<String> packageVersion() async { |
| 122 | final packageRoot = await resolvePackageRoot(); |
| 123 | final pubspecFile = File(packageRoot.resolve('pubspec.yaml').toFilePath()); |
| 124 | |
| 125 | if (!await pubspecFile.exists()) { |
| 126 | throw OndeCliException('Could not read pubspec.yaml for onde_cli.'); |
| 127 | } |
| 128 | |
| 129 | final lines = await pubspecFile.readAsLines(); |
| 130 | for (final line in lines) { |
| 131 | if (line.startsWith('version: ')) { |
| 132 | return line.substring('version: '.length).trim().replaceAll('"', ''); |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | throw OndeCliException('Could not find a version in pubspec.yaml.'); |
| 137 | } |
| 138 | |
| 139 | Future<Uri> resolvePackageRoot() async { |
| 140 | final packageUri = await Isolate.resolvePackageUri( |
| 141 | Uri.parse('package:onde_cli/onde_cli.dart'), |
| 142 | ); |
| 143 | |
| 144 | if (packageUri == null) { |
| 145 | throw OndeCliException( |
| 146 | 'Failed to resolve the installed Dart package path.', |
| 147 | ); |
| 148 | } |
| 149 | |
| 150 | return packageUri.resolve('../'); |
| 151 | } |
| 152 | |
| 153 | String runtimeIdentifier() { |
| 154 | final architecture = architectureName(); |
| 155 | |
| 156 | if (Platform.isMacOS) { |
| 157 | return 'darwin-$architecture'; |
| 158 | } |
| 159 | if (Platform.isWindows) { |
| 160 | return 'windows-$architecture'; |
| 161 | } |
| 162 | if (Platform.isLinux) { |
| 163 | return 'linux-$architecture'; |
| 164 | } |
| 165 | |
| 166 | throw OndeCliException( |
| 167 | 'onde does not support this operating system through the Dart package.', |
| 168 | ); |
| 169 | } |
| 170 | |
| 171 | String architectureName() { |
| 172 | final abi = Abi.current().toString().toLowerCase(); |
| 173 | if (abi.contains('arm64')) { |
| 174 | return 'arm64'; |
| 175 | } |
| 176 | if (abi.contains('x64')) { |
| 177 | return 'x64'; |
| 178 | } |
| 179 | |
| 180 | throw OndeCliException( |
| 181 | 'onde does not support the current architecture through the Dart package.', |
| 182 | ); |
| 183 | } |
| 184 | |
| 185 | String releaseAssetName() { |
| 186 | final architecture = architectureName() == 'x64' ? 'amd64' : 'arm64'; |
| 187 | |
| 188 | if (Platform.isMacOS) { |
| 189 | return 'onde-macos-$architecture'; |
| 190 | } |
| 191 | if (Platform.isLinux) { |
| 192 | return 'onde-linux-$architecture'; |
| 193 | } |
| 194 | if (Platform.isWindows) { |
| 195 | return 'onde-win-$architecture.exe'; |
| 196 | } |
| 197 | |
| 198 | throw OndeCliException( |
| 199 | 'onde does not support this operating system through the Dart package.', |
| 200 | ); |
| 201 | } |
| 202 | |
| 203 | String executableName() => Platform.isWindows ? 'onde.exe' : 'onde'; |
| 204 | |
| 205 | Future<void> ensureExecutablePermissions(String executablePath) async { |
| 206 | if (Platform.isWindows) { |
| 207 | return; |
| 208 | } |
| 209 | |
| 210 | final result = await Process.run('chmod', ['755', executablePath]); |
| 211 | if (result.exitCode != 0) { |
| 212 | throw OndeCliException( |
| 213 | 'Failed to mark the native onde binary as executable.', |
| 214 | ); |
| 215 | } |
| 216 | } |