diff --git a/.github/workflows/commit-built-file-changes.yml b/.github/workflows/commit-built-file-changes.yml index 64e734d05e8e8..a1df2bb9eb04f 100644 --- a/.github/workflows/commit-built-file-changes.yml +++ b/.github/workflows/commit-built-file-changes.yml @@ -1,13 +1,13 @@ # Commits all missed changes to built files back to pull request branches. name: Commit Built File Changes (PRs) -on: - workflow_run: - workflows: - - 'Check Built Files (PRs)' - - 'Test Default Themes & Create ZIPs' - types: - - completed +# on: +# workflow_run: +# workflows: +# - 'Check Built Files (PRs)' +# - 'Test Default Themes & Create ZIPs' +# types: +# - completed # Cancels all previous workflow runs for pull requests that have not completed. concurrency: diff --git a/composer.json b/composer.json index 4cd14415597c7..dc3cad30a0421 100644 --- a/composer.json +++ b/composer.json @@ -20,17 +20,147 @@ }, "require-dev": { "composer/ca-bundle": "1.5.8", + "james-heinrich/getid3": "1.9.23", + "paragonie/sodium_compat": "1.23.0", + "phpmailer/phpmailer": "6.11.1", + "rmccue/requests": "2.0.11", + "simplepie/simplepie": "1.9.0", "squizlabs/php_codesniffer": "3.13.2", "wp-coding-standards/wpcs": "~3.2.0", "phpcompatibility/phpcompatibility-wp": "~2.1.3", + "wordpress/custom-installer-plugin": "@dev", "yoast/phpunit-polyfills": "^1.1.0" }, + "repositories": [ + { + "type": "path", + "url": "tools/composer" + } + ], "config": { "allow-plugins": { + "wordpress/custom-installer-plugin": true, "dealerdirect/phpcodesniffer-composer-installer": true }, "lock": false }, + "extra": { + "installer-paths": { + "james-heinrich/getid3": { + "target": "src/wp-includes/ID3", + "source": "getid3", + "ignore": [ + "extension.cache.dbm.php", + "extension.cache.mysql.php", + "extension.cache.mysqli.php", + "extension.cache.sqlite3.php", + "module.archive.7zip.php", + "module.archive.gzip.php", + "module.archive.hpk.php", + "module.archive.rar.php", + "module.archive.szip.php", + "module.archive.tar.php", + "module.archive.xz.php", + "module.archive.zip.php", + "module.audio-video.bink.php", + "module.audio-video.ivf.php", + "module.audio-video.mpeg.php", + "module.audio-video.nsv.php", + "module.audio-video.real.php", + "module.audio-video.swf.php", + "module.audio-video.ts.php", + "module.audio-video.wtv.php", + "module.audio.aa.php", + "module.audio.aac.php", + "module.audio.amr.php", + "module.audio.au.php", + "module.audio.avr.php", + "module.audio.bonk.php", + "module.audio.dsdiff.php", + "module.audio.dsf.php", + "module.audio.dss.php", + "module.audio.la.php", + "module.audio.lpac.php", + "module.audio.midi.php", + "module.audio.mod.php", + "module.audio.monkey.php", + "module.audio.mpc.php", + "module.audio.optimfrog.php", + "module.audio.rkau.php", + "module.audio.shorten.php", + "module.audio.tak.php", + "module.audio.tta.php", + "module.audio.voc.php", + "module.audio.vqf.php", + "module.audio.wavpack.php", + "module.graphic.bmp.php", + "module.graphic.efax.php", + "module.graphic.gif.php", + "module.graphic.jpg.php", + "module.graphic.pcd.php", + "module.graphic.png.php", + "module.graphic.svg.php", + "module.graphic.tiff.php", + "module.misc.cue.php", + "module.misc.exe.php", + "module.misc.iso.php", + "module.misc.msoffice.php", + "module.misc.par2.php", + "module.misc.pdf.php", + "module.misc.torrent.php", + "module.tag.nikon-nctg.php", + "module.tag.xmp.php", + "write.apetag.php", + "write.id3v1.php", + "write.id3v2.php", + "write.lyrics3.php", + "write.metaflac.php", + "write.php", + "write.real.php", + "write.vorbiscomment.php" + ] + }, + "paragonie/sodium_compat": { + "target": "src/wp-includes/sodium_compat", + "ignore": [ + "README.md", + "composer-php52.json" + ] + }, + "phpmailer/phpmailer": { + "target": "src/wp-includes/PHPMailer", + "source": "src" + }, + "rmccue/requests": { + "target": "src/wp-includes/Requests", + "ignore": [ + ".editorconfig", + "certificates", + "CHANGELOG.md", + "composer.json", + "library/Deprecated.php", + "library/README.md", + "LICENSE", + "README.md" + ], + "replace": { + "library/Requests.php": "Requests.php" + } + }, + "simplepie/simplepie": { + "target": "src/wp-includes/SimplePie", + "ignore": [ + "CHANGELOG.md", + "composer.json", + "db.sql", + "LICENSES", + "phpstan.dist.neon", + "README.markdown", + "utils/PHPStan" + ] + } + } + }, "scripts": { "compat": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --standard=phpcompat.xml.dist --report=summary,source", "format": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf --report=summary,source", diff --git a/tools/composer/composer.json b/tools/composer/composer.json new file mode 100644 index 0000000000000..e2d05de9c96ed --- /dev/null +++ b/tools/composer/composer.json @@ -0,0 +1,17 @@ +{ + "name": "wordpress/custom-installer-plugin", + "description": "Custom Composer installer plugin for WordPress vendor dependencies", + "type": "composer-plugin", + "license": "GPL-2.0-or-later", + "require": { + "composer-plugin-api": "^2.0" + }, + "autoload": { + "psr-4": { + "WordPress\\Composer\\": "src/" + } + }, + "extra": { + "class": "WordPress\\Composer\\CustomInstallerPlugin" + } +} diff --git a/tools/composer/replacements/Requests.php b/tools/composer/replacements/Requests.php new file mode 100644 index 0000000000000..9e42db596b8c9 --- /dev/null +++ b/tools/composer/replacements/Requests.php @@ -0,0 +1,12 @@ +installerPaths = $this->composer->getPackage()->getExtra()['installer-paths']; + } + + /** + * Check if this installer supports the given package type. + * + * @param string $packageType The package type. + * @return bool + */ + public function supports( $packageType ) { + return true; + } + + /** + * Get the installation path for a package. + * + * @param PackageInterface $package The package. + * @return string The installation path. + */ + public function getInstallPath( PackageInterface $package ) { + $packageName = $package->getName(); + + if ( ! isset( $this->installerPaths[ $packageName ] ) ) { + return parent::getInstallPath( $package ); + } + + return realpath( getcwd() ) . '/' . $this->installerPaths[ $packageName ]['target']; + } + + /** + * Install a package. + * + * @param InstalledRepositoryInterface $repo The installed repository. + * @param PackageInterface $package The package to install. + * @return PromiseInterface|null + */ + public function install( InstalledRepositoryInterface $repo, PackageInterface $package ) { + $installer = parent::install( $repo, $package ); + + if ( $installer instanceof PromiseInterface ) { + return $installer->then( fn() => $this->modifyPaths( $package ) ); + } + + $this->modifyPaths( $package ); + return null; + } + + /** + * Update a package. + * + * @param InstalledRepositoryInterface $repo The installed repository. + * @param PackageInterface $initial The initial package. + * @param PackageInterface $target The target package. + * @return PromiseInterface|null + */ + public function update( InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target ) { + $updater = parent::update( $repo, $initial, $target ); + + if ( $updater instanceof PromiseInterface ) { + return $updater->then( fn() => $this->modifyPaths( $target ) ); + } + + $this->modifyPaths( $target ); + return null; + } + + /** + * Modify installation paths based on source subdirectory and ignore patterns. + * + * @param PackageInterface $package The package. + */ + private function modifyPaths( PackageInterface $package ): void { + $installPath = $this->getInstallPath( $package ); + + $this->applySourceSubdirectory( $package, $installPath ); + $this->applyReplacements( $package, $installPath ); + $this->applyIgnorePatterns( $package, $installPath ); + } + + /** + * Apply file replacements from custom replacement files. + * + * @param PackageInterface $package The package. + * @param string $installPath The installation path. + */ + private function applyReplacements( PackageInterface $package, string $installPath ): void { + $packageName = $package->getName(); + + if ( ! isset( $this->installerPaths[ $packageName ]['replace'] ) ) { + return; + } + + $filesystem = new Filesystem(); + $replacementsDir = realpath( getcwd() ) . '/tools/composer/replacements'; + + foreach ( $this->installerPaths[ $packageName ]['replace'] as $target => $source ) { + $sourcePath = $replacementsDir . '/' . $source; + + if ( ! file_exists( $sourcePath ) ) { + throw new \RuntimeException( "Replacement file 'tools/composer/replacements/{$source}' does not exist for package '{$packageName}'." ); + } + + $filesystem->copy( $sourcePath, $installPath . '/' . $target ); + } + } + + /** + * Apply ignore patterns to remove unwanted files after installation. + * + * @param PackageInterface $package The package. + * @param string $installPath The installation path. + */ + private function applyIgnorePatterns( PackageInterface $package, string $installPath ): void { + $packageName = $package->getName(); + + if ( ! isset( $this->installerPaths[ $packageName ]['ignore'] ) ) { + return; + } + + $filesystem = new Filesystem(); + + foreach ( $this->installerPaths[ $packageName ]['ignore'] as $pattern ) { + $matches = glob( $installPath . '/' . $pattern ); + + if ( empty( $matches ) ) { + throw new \RuntimeException( "Failed to glob pattern '{$pattern}' in package '{$package->getName()}'." ); + } + + foreach ( $matches as $path ) { + $filesystem->remove( $path ); + } + } + } + + /** + * Apply source subdirectory to flatten directory structure. + * + * @param PackageInterface $package The package. + * @param string $installPath The installation path. + */ + private function applySourceSubdirectory( PackageInterface $package, string $installPath ): void { + $packageName = $package->getName(); + + if ( ! isset( $this->installerPaths[ $packageName ]['source'] ) ) { + return; + } + + $sourceSubdir = $this->installerPaths[ $packageName ]['source']; + $sourceDir = $installPath . '/' . $sourceSubdir; + + if ( ! is_dir( $sourceDir ) ) { + throw new \RuntimeException( "Source directory '{$sourceSubdir}' does not exist in package '{$packageName}'." ); + } + + $filesystem = new Filesystem(); + $tempDir = $installPath . '_temp'; + + $filesystem->rename( $sourceDir, $tempDir ); + $filesystem->removeDirectory( $installPath ); + $filesystem->rename( $tempDir, $installPath ); + } +} diff --git a/tools/composer/src/CustomInstallerPlugin.php b/tools/composer/src/CustomInstallerPlugin.php new file mode 100644 index 0000000000000..afeba682f3407 --- /dev/null +++ b/tools/composer/src/CustomInstallerPlugin.php @@ -0,0 +1,46 @@ +getInstallationManager()->addInstaller( $installer ); + } + + /** + * Remove any hooks from Composer. + * + * @param Composer $composer The Composer instance. + * @param IOInterface $io The IO interface. + */ + public function deactivate( Composer $composer, IOInterface $io ) { + // Nothing to do here. + } + + /** + * Prepare the plugin to be uninstalled. + * + * @param Composer $composer The Composer instance. + * @param IOInterface $io The IO interface. + */ + public function uninstall( Composer $composer, IOInterface $io ) { + // Nothing to do here. + } +}