diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f298c3f..055d95d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,13 +52,14 @@ jobs: run: ./vendor/bin/phpunit --testsuite=Integration unit: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} needs: - php-cs-fixer strategy: matrix: php: [8.1, 8.2, 8.3] dependencies: [ 'lowest', 'highest' ] + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 @@ -79,4 +80,4 @@ jobs: run: | composer global require php-coveralls/php-coveralls php-coveralls --coverage_clover=clover.xml -v - if: ${{ github.event_name == 'push' && matrix.php == '8.1' && matrix.dependencies == 'highest' }} + if: ${{ github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.php == '8.1' && matrix.dependencies == 'highest' }} diff --git a/src/Installer/ExecutableInstaller.php b/src/Installer/ExecutableInstaller.php new file mode 100644 index 0000000..b3ef757 --- /dev/null +++ b/src/Installer/ExecutableInstaller.php @@ -0,0 +1,33 @@ +getExecutablePaths() as $path) { + if (Platform::isWindows() || Platform::isWindowsSubsystemForLinux()) { + $proxy = $path.'.bat'; + if (file_exists($proxy)) { + $this->io->writeError(sprintf(' Skipped installation of bin %s.bat proxy for package %s: a .bat proxy was already installed', $path, $extraDownload->getName())); + } else { + $caller = BinaryInstaller::determineBinaryCaller($path); + file_put_contents($proxy, '@'.$caller.' "%~dp0'.ProcessExecutor::escape(basename($proxy, '.bat')).'" %*'); + } + } else { + chmod($path, 0777 & ~umask()); + } + } + } +} diff --git a/src/Installer/ExecutableInstallerInterface.php b/src/Installer/ExecutableInstallerInterface.php new file mode 100644 index 0000000..b2015ea --- /dev/null +++ b/src/Installer/ExecutableInstallerInterface.php @@ -0,0 +1,10 @@ +createExtraDownload($parent, $this->hash); $this->assertSame( - realpath(__DIR__.'/../../../../vendor/composer/').'/../'.$parentName.'/'.$this->path, + realpath(__DIR__.'/../../../../vendor/composer/').'/../'.$parentName.\DIRECTORY_SEPARATOR.$this->path, $this->extraDownload->getInstallPath() ); } @@ -79,8 +79,8 @@ public function testGetExecutablePathsWhenParentPackageIsInstalled(): void $parent = new Package($parentName, 'any version', 'any pretty version'); $this->createExtraDownload($parent, $this->hash); $this->assertSame([ - realpath(__DIR__.'/../../../../vendor/composer/').'/../'.$parentName.'/file1', - realpath(__DIR__.'/../../../../vendor/composer/').'/../'.$parentName.'/path/to/file2', + realpath(__DIR__.'/../../../../vendor/composer/').'/../'.$parentName.\DIRECTORY_SEPARATOR.'file1', + realpath(__DIR__.'/../../../../vendor/composer/').'/../'.$parentName.\DIRECTORY_SEPARATOR.'path/to/file2', ], $this->extraDownload->getExecutablePaths()); } diff --git a/tests/Unit/Composer/Package/InstallPath/InstallPathTest.php b/tests/Unit/Composer/Package/InstallPath/InstallPathTest.php index 1939ae2..1d8002b 100644 --- a/tests/Unit/Composer/Package/InstallPath/InstallPathTest.php +++ b/tests/Unit/Composer/Package/InstallPath/InstallPathTest.php @@ -40,7 +40,7 @@ public function testConvertToAbsoluteWhenPackageIsInstalled(): void ->method('getName') ->willReturn($name); $this->assertSame( - realpath(__DIR__.'/../../../../../vendor/composer/').'/../'.$name.'/'.$this->relative, + realpath(__DIR__.'/../../../../../vendor/composer/').'/../'.$name.\DIRECTORY_SEPARATOR.$this->relative, $this->installPath->convertToAbsolute($this->relative) ); } diff --git a/tests/Unit/Installer/ExecutableInstallerTest.php b/tests/Unit/Installer/ExecutableInstallerTest.php new file mode 100644 index 0000000..d3210e8 --- /dev/null +++ b/tests/Unit/Installer/ExecutableInstallerTest.php @@ -0,0 +1,88 @@ + '/path/to/files/file1', + true => '/path/to/files/other/path/to/file2', + ]; + + protected function setUp(): void + { + $this->fs = new FileSystem(); + $this->io = $this->createMock(IOInterface::class); + $this->extraDownload = $this->createMock(ExtraDownloadInterface::class); + $this->installer = new ExecutableInstaller($this->io); + $this->createDirectoryAndFiles(); + } + + protected function tearDown(): void + { + $this->fs = null; + } + + public function testInstall(): void + { + $this->extraDownload + ->expects($this->once()) + ->method('getExecutablePaths') + ->willReturn(array_map(fn (string $path) => $this->fs->path($path), $this->executablePaths)); + if (\PHP_OS_FAMILY === 'Windows') { + $this->extraDownload + ->expects($this->once()) + ->method('getName') + ->willReturn($this->name); + $this->io + ->expects($this->once()) + ->method('writeError') + ->with(' Skipped installation of bin '.$this->fs->path('/path/to/files/other/path/to/file2.bat').' proxy for package '.$this->name.': a .bat proxy was already installed'); + } + $this->installer->install($this->extraDownload); + foreach ($this->executablePaths as $hasProxy => $path) { + if (\PHP_OS_FAMILY === 'Windows') { + $proxy = $path.'.bat'; + if (!$hasProxy) { + $this->assertStringEqualsFile( + $this->fs->path($proxy), + '@php "%~dp0file1" %*' + ); + } + } else { + $this->assertTrue(is_executable($this->fs->path($path))); + } + } + } + + private function createDirectoryAndFiles(): void + { + foreach ($this->executablePaths as $hasProxy => $path) { + $this->fs->createDirectory(\dirname($path), true); + $content = implode(\PHP_EOL, [ + '#!/usr/bin/env php', + 'fs->createFile($path, $content); + chmod($this->fs->path($path), 0600); + if (\PHP_OS_FAMILY === 'Windows' && $hasProxy) { + $proxy = $path.'.bat'; + $this->fs->createFile($proxy, 'proxy content'); + } + } + } +}