From bbe18f758053a8bfdd6ff95a04ecc34f73bce6d1 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Thu, 4 Jun 2020 14:00:33 -0700 Subject: [PATCH] use Expand-Archive and Compress-Archive in windows os utils (#58390) Work towards removal of package:archive and ideally more stable unzipping of artifacts. These commands are available in Powershell 5+, which we already require for windows. --- packages/flutter_tools/lib/src/base/os.dart | 48 +++---- .../test/general.shard/base/os_test.dart | 127 ++++++++++++++++++ 2 files changed, 152 insertions(+), 23 deletions(-) diff --git a/packages/flutter_tools/lib/src/base/os.dart b/packages/flutter_tools/lib/src/base/os.dart index 2f7e242ecb..99d00807c2 100644 --- a/packages/flutter_tools/lib/src/base/os.dart +++ b/packages/flutter_tools/lib/src/base/os.dart @@ -291,7 +291,15 @@ class _WindowsUtils extends OperatingSystemUtils { logger: logger, platform: platform, processManager: processManager, - ); + ) { + if (processManager.canRun('pwsh.exe')) { + _activePowershell = 'pwsh.exe'; + } else { + _activePowershell = 'PowerShell.exe'; + } + } + + String _activePowershell; @override void makeExecutable(File file) {} @@ -315,36 +323,30 @@ class _WindowsUtils extends OperatingSystemUtils { @override void zip(Directory data, File zipFile) { - final Archive archive = Archive(); - for (final FileSystemEntity entity in data.listSync(recursive: true)) { - if (entity is! File) { - continue; - } - final File file = entity as File; - final String path = file.fileSystem.path.relative(file.path, from: data.path); - final List bytes = file.readAsBytesSync(); - archive.addFile(ArchiveFile(path, bytes.length, bytes)); + final RunResult result = _processUtils.runSync([ + _activePowershell, + '-command', + '"Compress-Archive ${data.path} -DestinationPath ${zipFile.path}"', + ]); + if (result.stderr.isNotEmpty) { + throw ProcessException(_activePowershell, ['Compress-Archive'], result.stderr); } - zipFile.writeAsBytesSync(ZipEncoder().encode(archive), flush: true); } @override void unzip(File file, Directory targetDirectory) { - final Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync()); - _unpackArchive(archive, targetDirectory); + final RunResult result = _processUtils.runSync([ + _activePowershell, + '-command', + '"Expand-Archive ${file.path} -DestinationPath ${targetDirectory.path}"', + ], throwOnError: true); + if (result.stderr.isNotEmpty) { + throw ProcessException(_activePowershell, ['Expand-Archive'], result.stderr); + } } @override - bool verifyZip(File zipFile) { - try { - ZipDecoder().decodeBytes(zipFile.readAsBytesSync(), verify: true); - } on FileSystemException catch (_) { - return false; - } on ArchiveException catch (_) { - return false; - } - return true; - } + bool verifyZip(File zipFile) => true; @override void unpack(File gzippedTarFile, Directory targetDirectory) { diff --git a/packages/flutter_tools/test/general.shard/base/os_test.dart b/packages/flutter_tools/test/general.shard/base/os_test.dart index 6013ea0db6..5de6e9b79a 100644 --- a/packages/flutter_tools/test/general.shard/base/os_test.dart +++ b/packages/flutter_tools/test/general.shard/base/os_test.dart @@ -21,6 +21,17 @@ const String kExecutable = 'foo'; const String kPath1 = '/bar/bin/$kExecutable'; const String kPath2 = '/another/bin/$kExecutable'; +const String kPowershellException = r''' +New-Object : Exception calling ".ctor" with "3" argument(s): "End of Central Directory record could not be found." +At +C:\Windows\system32\WindowsPowerShell\v1.0\Modules\Microsoft.PowerShell.Archive\Microsoft.PowerShell.Archive.psm1:934 +char:23 ++ ... ipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -Ar ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : InvalidOperation: (:) [New-Object], MethodInvocationException + + FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.PowerShell.Commands.NewObjectCommand +'''; + void main() { MockProcessManager mockProcessManager; @@ -67,6 +78,7 @@ void main() { testWithoutContext('returns null when executable does not exist', () async { when(mockProcessManager.runSync(['where', kExecutable])) .thenReturn(ProcessResult(0, 1, null, null)); + when(mockProcessManager.canRun('pwsh.exe')).thenReturn(true); final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'windows')); expect(utils.which(kExecutable), isNull); }); @@ -74,6 +86,7 @@ void main() { testWithoutContext('returns exactly one result', () async { when(mockProcessManager.runSync(['where', 'foo'])) .thenReturn(ProcessResult(0, 0, '$kPath1\n$kPath2', null)); + when(mockProcessManager.canRun('pwsh.exe')).thenReturn(true); final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'windows')); expect(utils.which(kExecutable).path, kPath1); }); @@ -81,6 +94,7 @@ void main() { testWithoutContext('returns all results for whichAll', () async { when(mockProcessManager.runSync(['where', kExecutable])) .thenReturn(ProcessResult(0, 0, '$kPath1\n$kPath2', null)); + when(mockProcessManager.canRun('pwsh.exe')).thenReturn(true); final OperatingSystemUtils utils = createOSUtils(FakePlatform(operatingSystem: 'windows')); final List result = utils.whichAll(kExecutable); expect(result, hasLength(2)); @@ -97,6 +111,7 @@ void main() { when(mockFile.readAsBytesSync()).thenThrow( const FileSystemException('error'), ); + when(mockProcessManager.canRun('pwsh.exe')).thenReturn(true); final OperatingSystemUtils osUtils = OperatingSystemUtils( fileSystem: fileSystem, logger: BufferLogger.test(), @@ -116,6 +131,7 @@ void main() { 0x01, 0x02, ])); + when(mockProcessManager.canRun('pwsh.exe')).thenReturn(true); final OperatingSystemUtils osUtils = OperatingSystemUtils( fileSystem: fileSystem, logger: BufferLogger.test(), @@ -131,6 +147,7 @@ void main() { final MockFile mockFile = MockFile(); when(fileSystem.file(any)).thenReturn(mockFile); when(mockFile.readAsBytesSync()).thenReturn(Uint8List(0)); + when(mockProcessManager.canRun('pwsh.exe')).thenReturn(true); final OperatingSystemUtils osUtils = OperatingSystemUtils( fileSystem: fileSystem, logger: BufferLogger.test(), @@ -142,6 +159,116 @@ void main() { }); }); + testWithoutContext('Windows PowerShell Expand-Archive', () async { + final FakeProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'pwsh.exe', + '-command', + '"Expand-Archive a -DestinationPath b"', + ], + ), + ]); + final FileSystem fileSystem = MemoryFileSystem.test(style: FileSystemStyle.windows); + final OperatingSystemUtils osUtils = OperatingSystemUtils( + fileSystem: fileSystem, + logger: BufferLogger.test(), + platform: FakePlatform(operatingSystem: 'windows'), + processManager: processManager, + ); + + osUtils.unzip(fileSystem.file('a'), fileSystem.directory('b')); + + expect(processManager.hasRemainingExpectations, false); + }); + + testWithoutContext('Windows PowerShell Expand-Archive with stderr', () async { + final FakeProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'pwsh.exe', + '-command', + '"Expand-Archive a -DestinationPath b"', + ], + stderr: kPowershellException, + ), + ]); + final FileSystem fileSystem = MemoryFileSystem.test(style: FileSystemStyle.windows); + final OperatingSystemUtils osUtils = OperatingSystemUtils( + fileSystem: fileSystem, + logger: BufferLogger.test(), + platform: FakePlatform(operatingSystem: 'windows'), + processManager: processManager, + ); + + expect(() => osUtils.unzip(fileSystem.file('a'), fileSystem.directory('b')), + throwsA(isA())); + + expect(processManager.hasRemainingExpectations, false); + }); + + testWithoutContext('Windows PowerShell Compress-Archive', () { + final FakeProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'pwsh.exe', + '-command', + '"Compress-Archive b -DestinationPath a"', + ], + ), + ]); + final FileSystem fileSystem = MemoryFileSystem.test(style: FileSystemStyle.windows); + final OperatingSystemUtils osUtils = OperatingSystemUtils( + fileSystem: fileSystem, + logger: BufferLogger.test(), + platform: FakePlatform(operatingSystem: 'windows'), + processManager: processManager, + ); + + osUtils.zip(fileSystem.directory('b'), fileSystem.file('a')); + + expect(processManager.hasRemainingExpectations, false); + }); + + testWithoutContext('Windows PowerShell Compress-Archive with stderr', () { + final FakeProcessManager processManager = FakeProcessManager.list([ + const FakeCommand( + command: [ + 'pwsh.exe', + '-command', + '"Compress-Archive b -DestinationPath a"', + ], + stderr: kPowershellException, + ), + ]); + final FileSystem fileSystem = MemoryFileSystem.test(style: FileSystemStyle.windows); + final OperatingSystemUtils osUtils = OperatingSystemUtils( + fileSystem: fileSystem, + logger: BufferLogger.test(), + platform: FakePlatform(operatingSystem: 'windows'), + processManager: processManager, + ); + + expect(() => osUtils.zip(fileSystem.directory('b'), fileSystem.file('a')), + throwsA(isA())); + + expect(processManager.hasRemainingExpectations, false); + }); + + testWithoutContext('Windows PowerShell verifyZip is a no-op', () { + final FakeProcessManager processManager = FakeProcessManager.list([]); + final FileSystem fileSystem = MemoryFileSystem.test(style: FileSystemStyle.windows); + final OperatingSystemUtils osUtils = OperatingSystemUtils( + fileSystem: fileSystem, + logger: BufferLogger.test(), + platform: FakePlatform(operatingSystem: 'windows'), + processManager: processManager, + ); + + expect(osUtils.verifyZip(fileSystem.file('a')), true); + expect(processManager.hasRemainingExpectations, false); + }); + testWithoutContext('stream compression level', () { expect(OperatingSystemUtils.gzipLevel1.level, equals(1)); });