os.dart 12.4 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
  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,
      );

55 56 57
  @visibleForTesting
  static final GZipCodec gzipLevel1 = GZipCodec(level: 1);

58 59 60 61 62
  final FileSystem _fileSystem;
  final Logger _logger;
  final Platform _platform;
  final ProcessManager _processManager;
  final ProcessUtils _processUtils;
63

64
  /// Make the given file executable. This may be a no-op on some platforms.
65 66 67 68 69 70 71 72 73
  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);
74

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

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

89 90 91
  /// Return the File representing a new pipe.
  File makePipe(String path);

92 93
  void zip(Directory data, File zipFile);

94
  void unzip(File file, Directory targetDirectory);
95

96 97 98
  /// Returns true if the ZIP is not corrupt.
  bool verifyZip(File file);

99 100
  void unpack(File gzippedTarFile, Directory targetDirectory);

101 102 103
  /// Returns true if the gzip is not corrupt (does not check tar).
  bool verifyGzip(File gzippedFile);

104 105 106 107 108
  /// Compresses a stream using gzip level 1 (faster but larger).
  Stream<List<int>> gzipLevel1Stream(Stream<List<int>> stream) {
    return stream.cast<List<int>>().transform<List<int>>(gzipLevel1.encoder);
  }

109 110 111 112
  /// Returns a pretty name string for the current operating system.
  ///
  /// If available, the detailed version of the OS is included.
  String get name {
113
    const Map<String, String> osNames = <String, String>{
114 115
      'macos': 'Mac OS',
      'linux': 'Linux',
116
      'windows': 'Windows',
117
    };
118
    final String osName = _platform.operatingSystem;
119 120 121
    return osNames.containsKey(osName) ? osNames[osName] : osName;
  }

122
  List<File> _which(String execName, { bool all = false });
123 124 125

  /// Returns the separator between items in the PATH environment variable.
  String get pathVarSeparator;
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145

  /// 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);
      }
146
      _logger.printTrace('findFreePort failed: $e');
147
    } on Exception catch (e) {
148
      // Failures are signaled by a return value of 0 from this function.
149
      _logger.printTrace('findFreePort failed: $e');
150 151 152 153 154 155 156
    } finally {
      if (serverSocket != null) {
        await serverSocket.close();
      }
    }
    return port;
  }
157 158
}

159
class _PosixUtils extends OperatingSystemUtils {
160 161 162 163 164 165 166 167 168 169 170
  _PosixUtils({
    @required FileSystem fileSystem,
    @required Logger logger,
    @required Platform platform,
    @required ProcessManager processManager,
  }) : super._private(
    fileSystem: fileSystem,
    logger: logger,
    platform: platform,
    processManager: processManager,
  );
171

172
  @override
173 174 175 176 177 178 179
  void makeExecutable(File file) {
    chmod(file, 'a+x');
  }

  @override
  void chmod(FileSystemEntity entity, String mode) {
    try {
180 181 182
      final ProcessResult result = _processManager.runSync(
        <String>['chmod', mode, entity.path],
      );
183
      if (result.exitCode != 0) {
184
        _logger.printTrace(
185 186 187 188 189 190
          'Error trying to run chmod on ${entity.absolute.path}'
          '\nstdout: ${result.stdout}'
          '\nstderr: ${result.stderr}',
        );
      }
    } on ProcessException catch (error) {
191 192 193
      _logger.printTrace(
        'Error trying to run chmod on ${entity.absolute.path}: $error',
      );
194
    }
195
  }
196

197
  @override
198
  List<File> _which(String execName, { bool all = false }) {
199 200 201 202 203
    final List<String> command = <String>[
      'which',
      if (all) '-a',
      execName,
    ];
204
    final ProcessResult result = _processManager.runSync(command);
205
    if (result.exitCode != 0) {
206
      return const <File>[];
207
    }
208
    final String stdout = result.stdout as String;
209 210 211
    return stdout.trim().split('\n').map<File>(
      (String path) => _fileSystem.file(path.trim()),
    ).toList();
212
  }
213

214 215
  @override
  void zip(Directory data, File zipFile) {
216
    _processUtils.runSync(
217 218 219 220
      <String>['zip', '-r', '-q', zipFile.path, '.'],
      workingDirectory: data.path,
      throwOnError: true,
    );
221 222
  }

223 224 225
  // unzip -o -q zipfile -d dest
  @override
  void unzip(File file, Directory targetDirectory) {
226
    _processUtils.runSync(
227 228 229
      <String>['unzip', '-o', '-q', file.path, '-d', targetDirectory.path],
      throwOnError: true,
    );
230
  }
231

232
  @override
233
  bool verifyZip(File zipFile) =>
234
    _processUtils.exitsHappySync(<String>['unzip', '-t', '-qq', zipFile.path]);
235

236 237 238
  // tar -xzf tarball -C dest
  @override
  void unpack(File gzippedTarFile, Directory targetDirectory) {
239
    _processUtils.runSync(
240 241 242
      <String>['tar', '-xzf', gzippedTarFile.path, '-C', targetDirectory.path],
      throwOnError: true,
    );
243 244
  }

245
  @override
246
  bool verifyGzip(File gzippedFile) =>
247
    _processUtils.exitsHappySync(<String>['gzip', '-t', gzippedFile.path]);
248

249 250
  @override
  File makePipe(String path) {
251
    _processUtils.runSync(
252 253 254
      <String>['mkfifo', path],
      throwOnError: true,
    );
255
    return _fileSystem.file(path);
256
  }
257 258 259 260 261

  String _name;

  @override
  String get name {
262 263 264 265 266 267 268 269 270 271 272 273 274
    if (_name == null) {
      if (_platform.isMacOS) {
        final List<RunResult> results = <RunResult>[
          _processUtils.runSync(<String>['sw_vers', '-productName']),
          _processUtils.runSync(<String>['sw_vers', '-productVersion']),
          _processUtils.runSync(<String>['sw_vers', '-buildVersion']),
        ];
        if (results.every((RunResult result) => result.exitCode == 0)) {
          _name = '${results[0].stdout.trim()} ${results[1].stdout
              .trim()} ${results[2].stdout.trim()}';
        }
      }
      _name ??= super.name;
275
    }
276
    return _name;
277
  }
278 279 280

  @override
  String get pathVarSeparator => ':';
281 282
}

283
class _WindowsUtils extends OperatingSystemUtils {
284 285 286 287 288 289 290 291 292 293 294
  _WindowsUtils({
    @required FileSystem fileSystem,
    @required Logger logger,
    @required Platform platform,
    @required ProcessManager processManager,
  }) : super._private(
    fileSystem: fileSystem,
    logger: logger,
    platform: platform,
    processManager: processManager,
  );
295

296
  @override
297 298 299 300
  void makeExecutable(File file) {}

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

302
  @override
303
  List<File> _which(String execName, { bool all = false }) {
304
    // `where` always returns all matches, not just the first one.
305
    final ProcessResult result = _processManager.runSync(<String>['where', execName]);
306
    if (result.exitCode != 0) {
307
      return const <File>[];
308
    }
309
    final List<String> lines = (result.stdout as String).trim().split('\n');
310
    if (all) {
311
      return lines.map<File>((String path) => _fileSystem.file(path.trim())).toList();
312
    }
313
    return <File>[_fileSystem.file(lines.first.trim())];
314 315
  }

316 317
  @override
  void zip(Directory data, File zipFile) {
318
    final Archive archive = Archive();
319
    for (final FileSystemEntity entity in data.listSync(recursive: true)) {
320 321 322
      if (entity is! File) {
        continue;
      }
323
      final File file = entity as File;
324 325
      final String path = file.fileSystem.path.relative(file.path, from: data.path);
      final List<int> bytes = file.readAsBytesSync();
326
      archive.addFile(ArchiveFile(path, bytes.length, bytes));
327
    }
328
    zipFile.writeAsBytesSync(ZipEncoder().encode(archive), flush: true);
329 330
  }

331 332
  @override
  void unzip(File file, Directory targetDirectory) {
333
    final Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());
334 335 336
    _unpackArchive(archive, targetDirectory);
  }

337 338 339
  @override
  bool verifyZip(File zipFile) {
    try {
340
      ZipDecoder().decodeBytes(zipFile.readAsBytesSync(), verify: true);
341 342 343 344 345 346 347 348
    } on FileSystemException catch (_) {
      return false;
    } on ArchiveException catch (_) {
      return false;
    }
    return true;
  }

349 350
  @override
  void unpack(File gzippedTarFile, Directory targetDirectory) {
351 352
    final Archive archive = TarDecoder().decodeBytes(
      GZipDecoder().decodeBytes(gzippedTarFile.readAsBytesSync()),
353 354 355
    );
    _unpackArchive(archive, targetDirectory);
  }
356

357 358 359
  @override
  bool verifyGzip(File gzipFile) {
    try {
360
      GZipDecoder().decodeBytes(gzipFile.readAsBytesSync(), verify: true);
361 362 363 364
    } on FileSystemException catch (_) {
      return false;
    } on ArchiveException catch (_) {
      return false;
365 366
    } on RangeError catch (_) {
      return false;
367 368 369 370
    }
    return true;
  }

371
  void _unpackArchive(Archive archive, Directory targetDirectory) {
372
    for (final ArchiveFile archiveFile in archive.files) {
373
      // The archive package doesn't correctly set isFile.
374
      if (!archiveFile.isFile || archiveFile.name.endsWith('/')) {
375
        continue;
376
      }
377

378 379 380 381
      final File destFile = _fileSystem.file(_fileSystem.path.join(
        targetDirectory.path,
        archiveFile.name,
      ));
382
      if (!destFile.parent.existsSync()) {
383
        destFile.parent.createSync(recursive: true);
384
      }
385
      destFile.writeAsBytesSync(archiveFile.content as List<int>);
386
    }
387
  }
388 389 390

  @override
  File makePipe(String path) {
391
    throw UnsupportedError('makePipe is not implemented on Windows.');
392
  }
393 394 395 396 397 398

  String _name;

  @override
  String get name {
    if (_name == null) {
399
      final ProcessResult result = _processManager.runSync(
400
          <String>['ver'], runInShell: true);
401
      if (result.exitCode == 0) {
402
        _name = (result.stdout as String).trim();
403
      } else {
404
        _name = super.name;
405
      }
406 407 408
    }
    return _name;
  }
409 410 411

  @override
  String get pathVarSeparator => ';';
412
}
413

414 415
/// Find and return the project root directory relative to the specified
/// directory or the current working directory if none specified.
416
/// Return null if the project root could not be found
417
/// or if the project root is the flutter repository root.
418
String findProjectRoot([ String directory ]) {
419
  const String kProjectRootSentinel = 'pubspec.yaml';
420
  directory ??= globals.fs.currentDirectory.path;
421
  while (true) {
422
    if (globals.fs.isFileSync(globals.fs.path.join(directory, kProjectRootSentinel))) {
423
      return directory;
424
    }
425
    final String parent = globals.fs.path.dirname(directory);
426
    if (directory == parent) {
427
      return null;
428
    }
429 430 431
    directory = parent;
  }
}