os.dart 12 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:archive/archive.dart';
6 7 8 9
import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
10

11
import '../globals.dart' as globals;
12
import 'file_system.dart';
13
import 'io.dart';
14
import 'logger.dart';
15
import 'process.dart';
16

17
abstract class OperatingSystemUtils {
18 19 20 21 22 23 24 25 26 27 28 29 30
  factory OperatingSystemUtils({
    @required FileSystem fileSystem,
    @required Logger logger,
    @required Platform platform,
    @required ProcessManager processManager,
  }) {
    if (platform.isWindows) {
      return _WindowsUtils(
        fileSystem: fileSystem,
        logger: logger,
        platform: platform,
        processManager: processManager,
      );
31
    } else {
32 33 34 35 36 37
      return _PosixUtils(
        fileSystem: fileSystem,
        logger: logger,
        platform: platform,
        processManager: processManager,
      );
38 39 40
    }
  }

41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
  OperatingSystemUtils._private({
    @required FileSystem fileSystem,
    @required Logger logger,
    @required Platform platform,
    @required ProcessManager processManager,
  }) : _fileSystem = fileSystem,
       _logger = logger,
       _platform = platform,
       _processManager = processManager,
       _processUtils = ProcessUtils(
        logger: logger,
        processManager: processManager,
      );

  final FileSystem _fileSystem;
  final Logger _logger;
  final Platform _platform;
  final ProcessManager _processManager;
  final ProcessUtils _processUtils;
60

61
  /// Make the given file executable. This may be a no-op on some platforms.
62 63 64 65 66 67 68 69 70
  void makeExecutable(File file);

  /// Updates the specified file system [entity] to have the file mode
  /// bits set to the value defined by [mode], which can be specified in octal
  /// (e.g. `644`) or symbolically (e.g. `u+x`).
  ///
  /// On operating systems that do not support file mode bits, this will be a
  /// no-op.
  void chmod(FileSystemEntity entity, String mode);
71

72
  /// Return the path (with symlinks resolved) to the given executable, or null
73
  /// if `which` was not able to locate the binary.
74 75
  File which(String execName) {
    final List<File> result = _which(execName);
76
    if (result == null || result.isEmpty) {
77
      return null;
78
    }
79 80
    return result.first;
  }
81

82 83
  /// Return a list of all paths to `execName` found on the system. Uses the
  /// PATH environment variable.
84
  List<File> whichAll(String execName) => _which(execName, all: true);
85

86 87 88
  /// Return the File representing a new pipe.
  File makePipe(String path);

89 90
  void zip(Directory data, File zipFile);

91
  void unzip(File file, Directory targetDirectory);
92

93 94 95
  /// Returns true if the ZIP is not corrupt.
  bool verifyZip(File file);

96 97
  void unpack(File gzippedTarFile, Directory targetDirectory);

98 99 100
  /// Returns true if the gzip is not corrupt (does not check tar).
  bool verifyGzip(File gzippedFile);

101 102 103 104
  /// Returns a pretty name string for the current operating system.
  ///
  /// If available, the detailed version of the OS is included.
  String get name {
105
    const Map<String, String> osNames = <String, String>{
106 107
      'macos': 'Mac OS',
      'linux': 'Linux',
108
      'windows': 'Windows',
109
    };
110
    final String osName = _platform.operatingSystem;
111 112 113
    return osNames.containsKey(osName) ? osNames[osName] : osName;
  }

114
  List<File> _which(String execName, { bool all = false });
115 116 117

  /// Returns the separator between items in the PATH environment variable.
  String get pathVarSeparator;
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137

  /// Returns an unused network port.
  ///
  /// Returns 0 if an unused port cannot be found.
  ///
  /// The port returned by this function may become used before it is bound by
  /// its intended user.
  Future<int> findFreePort({bool ipv6 = false}) async {
    int port = 0;
    ServerSocket serverSocket;
    final InternetAddress loopback =
        ipv6 ? InternetAddress.loopbackIPv6 : InternetAddress.loopbackIPv4;
    try {
      serverSocket = await ServerSocket.bind(loopback, 0);
      port = serverSocket.port;
    } on SocketException catch (e) {
      // If ipv4 loopback bind fails, try ipv6.
      if (!ipv6) {
        return findFreePort(ipv6: true);
      }
138
      _logger.printTrace('findFreePort failed: $e');
139 140
    } catch (e) {
      // Failures are signaled by a return value of 0 from this function.
141
      _logger.printTrace('findFreePort failed: $e');
142 143 144 145 146 147 148
    } finally {
      if (serverSocket != null) {
        await serverSocket.close();
      }
    }
    return port;
  }
149 150
}

151
class _PosixUtils extends OperatingSystemUtils {
152 153 154 155 156 157 158 159 160 161 162
  _PosixUtils({
    @required FileSystem fileSystem,
    @required Logger logger,
    @required Platform platform,
    @required ProcessManager processManager,
  }) : super._private(
    fileSystem: fileSystem,
    logger: logger,
    platform: platform,
    processManager: processManager,
  );
163

164
  @override
165 166 167 168 169 170 171
  void makeExecutable(File file) {
    chmod(file, 'a+x');
  }

  @override
  void chmod(FileSystemEntity entity, String mode) {
    try {
172 173 174
      final ProcessResult result = _processManager.runSync(
        <String>['chmod', mode, entity.path],
      );
175
      if (result.exitCode != 0) {
176
        _logger.printTrace(
177 178 179 180 181 182
          'Error trying to run chmod on ${entity.absolute.path}'
          '\nstdout: ${result.stdout}'
          '\nstderr: ${result.stderr}',
        );
      }
    } on ProcessException catch (error) {
183 184 185
      _logger.printTrace(
        'Error trying to run chmod on ${entity.absolute.path}: $error',
      );
186
    }
187
  }
188

189
  @override
190
  List<File> _which(String execName, { bool all = false }) {
191 192 193 194 195
    final List<String> command = <String>[
      'which',
      if (all) '-a',
      execName,
    ];
196
    final ProcessResult result = _processManager.runSync(command);
197
    if (result.exitCode != 0) {
198
      return const <File>[];
199
    }
200
    final String stdout = result.stdout as String;
201 202 203
    return stdout.trim().split('\n').map<File>(
      (String path) => _fileSystem.file(path.trim()),
    ).toList();
204
  }
205

206 207
  @override
  void zip(Directory data, File zipFile) {
208
    _processUtils.runSync(
209 210 211 212
      <String>['zip', '-r', '-q', zipFile.path, '.'],
      workingDirectory: data.path,
      throwOnError: true,
    );
213 214
  }

215 216 217
  // unzip -o -q zipfile -d dest
  @override
  void unzip(File file, Directory targetDirectory) {
218
    _processUtils.runSync(
219 220 221
      <String>['unzip', '-o', '-q', file.path, '-d', targetDirectory.path],
      throwOnError: true,
    );
222
  }
223

224
  @override
225
  bool verifyZip(File zipFile) =>
226
    _processUtils.exitsHappySync(<String>['zip', '-T', zipFile.path]);
227

228 229 230
  // tar -xzf tarball -C dest
  @override
  void unpack(File gzippedTarFile, Directory targetDirectory) {
231
    _processUtils.runSync(
232 233 234
      <String>['tar', '-xzf', gzippedTarFile.path, '-C', targetDirectory.path],
      throwOnError: true,
    );
235 236
  }

237
  @override
238
  bool verifyGzip(File gzippedFile) =>
239
    _processUtils.exitsHappySync(<String>['gzip', '-t', gzippedFile.path]);
240

241 242
  @override
  File makePipe(String path) {
243
    _processUtils.runSync(
244 245 246
      <String>['mkfifo', path],
      throwOnError: true,
    );
247
    return _fileSystem.file(path);
248
  }
249 250 251 252 253 254

  String _name;

  @override
  String get name {
    if (_name == null) {
255
      if (_platform.isMacOS) {
256
        final List<RunResult> results = <RunResult>[
257 258 259
          _processUtils.runSync(<String>['sw_vers', '-productName']),
          _processUtils.runSync(<String>['sw_vers', '-productVersion']),
          _processUtils.runSync(<String>['sw_vers', '-buildVersion']),
260
        ];
261
        if (results.every((RunResult result) => result.exitCode == 0)) {
262 263
          _name = '${results[0].stdout.trim()} ${results[1].stdout
              .trim()} ${results[2].stdout.trim()}';
264 265
        }
      }
266
      _name ??= super.name;
267 268 269
    }
    return _name;
  }
270 271 272

  @override
  String get pathVarSeparator => ':';
273 274
}

275
class _WindowsUtils extends OperatingSystemUtils {
276 277 278 279 280 281 282 283 284 285 286
  _WindowsUtils({
    @required FileSystem fileSystem,
    @required Logger logger,
    @required Platform platform,
    @required ProcessManager processManager,
  }) : super._private(
    fileSystem: fileSystem,
    logger: logger,
    platform: platform,
    processManager: processManager,
  );
287

288
  @override
289 290 291 292
  void makeExecutable(File file) {}

  @override
  void chmod(FileSystemEntity entity, String mode) {}
293

294
  @override
295
  List<File> _which(String execName, { bool all = false }) {
296
    // `where` always returns all matches, not just the first one.
297
    final ProcessResult result = _processManager.runSync(<String>['where', execName]);
298
    if (result.exitCode != 0) {
299
      return const <File>[];
300
    }
301
    final List<String> lines = (result.stdout as String).trim().split('\n');
302
    if (all) {
303
      return lines.map<File>((String path) => _fileSystem.file(path.trim())).toList();
304
    }
305
    return <File>[_fileSystem.file(lines.first.trim())];
306 307
  }

308 309
  @override
  void zip(Directory data, File zipFile) {
310
    final Archive archive = Archive();
311
    for (final FileSystemEntity entity in data.listSync(recursive: true)) {
312 313 314
      if (entity is! File) {
        continue;
      }
315
      final File file = entity as File;
316 317
      final String path = file.fileSystem.path.relative(file.path, from: data.path);
      final List<int> bytes = file.readAsBytesSync();
318
      archive.addFile(ArchiveFile(path, bytes.length, bytes));
319
    }
320
    zipFile.writeAsBytesSync(ZipEncoder().encode(archive), flush: true);
321 322
  }

323 324
  @override
  void unzip(File file, Directory targetDirectory) {
325
    final Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
326 327 328
    _unpackArchive(archive, targetDirectory);
  }

329 330 331
  @override
  bool verifyZip(File zipFile) {
    try {
332
      ZipDecoder().decodeBytes(zipFile.readAsBytesSync(), verify: true);
333 334 335 336 337 338 339 340
    } on FileSystemException catch (_) {
      return false;
    } on ArchiveException catch (_) {
      return false;
    }
    return true;
  }

341 342
  @override
  void unpack(File gzippedTarFile, Directory targetDirectory) {
343 344
    final Archive archive = TarDecoder().decodeBytes(
      GZipDecoder().decodeBytes(gzippedTarFile.readAsBytesSync()),
345 346 347
    );
    _unpackArchive(archive, targetDirectory);
  }
348

349 350 351
  @override
  bool verifyGzip(File gzipFile) {
    try {
352
      GZipDecoder().decodeBytes(gzipFile.readAsBytesSync(), verify: true);
353 354 355 356 357 358 359 360
    } on FileSystemException catch (_) {
      return false;
    } on ArchiveException catch (_) {
      return false;
    }
    return true;
  }

361
  void _unpackArchive(Archive archive, Directory targetDirectory) {
362
    for (final ArchiveFile archiveFile in archive.files) {
363
      // The archive package doesn't correctly set isFile.
364
      if (!archiveFile.isFile || archiveFile.name.endsWith('/')) {
365
        continue;
366
      }
367

368 369 370 371
      final File destFile = _fileSystem.file(_fileSystem.path.join(
        targetDirectory.path,
        archiveFile.name,
      ));
372
      if (!destFile.parent.existsSync()) {
373
        destFile.parent.createSync(recursive: true);
374
      }
375
      destFile.writeAsBytesSync(archiveFile.content as List<int>);
376
    }
377
  }
378 379 380

  @override
  File makePipe(String path) {
381
    throw UnsupportedError('makePipe is not implemented on Windows.');
382
  }
383 384 385 386 387 388

  String _name;

  @override
  String get name {
    if (_name == null) {
389
      final ProcessResult result = _processManager.runSync(
390
          <String>['ver'], runInShell: true);
391
      if (result.exitCode == 0) {
392
        _name = (result.stdout as String).trim();
393
      } else {
394
        _name = super.name;
395
      }
396 397 398
    }
    return _name;
  }
399 400 401

  @override
  String get pathVarSeparator => ';';
402
}
403

404 405
/// Find and return the project root directory relative to the specified
/// directory or the current working directory if none specified.
406
/// Return null if the project root could not be found
407
/// or if the project root is the flutter repository root.
408
String findProjectRoot([ String directory ]) {
409
  const String kProjectRootSentinel = 'pubspec.yaml';
410
  directory ??= globals.fs.currentDirectory.path;
411
  while (true) {
412
    if (globals.fs.isFileSync(globals.fs.path.join(directory, kProjectRootSentinel))) {
413
      return directory;
414
    }
415
    final String parent = globals.fs.path.dirname(directory);
416
    if (directory == parent) {
417
      return null;
418
    }
419 420 421
    directory = parent;
  }
}