From 4b1587b612d2bd8203edcb3a245e4733c8251903 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:42:23 +0000 Subject: [PATCH 1/3] Initial plan From bba838d16ce5063334e2eb17560863601d343a9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:47:29 +0000 Subject: [PATCH 2/3] Replace external zip command with PHP ZipArchive for cross-platform support Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- composer.json | 1 + src/Dist_Archive_Command.php | 144 ++++++++++++++++++++++++----------- 2 files changed, 99 insertions(+), 46 deletions(-) diff --git a/composer.json b/composer.json index ae19352..bc13dfd 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ ], "require": { "php": ">=7.2", + "ext-zip": "*", "wp-cli/wp-cli": "^2.13", "inmarelibero/gitignore-checker": "^1.0.4" }, diff --git a/src/Dist_Archive_Command.php b/src/Dist_Archive_Command.php index a2ed5fe..6de8769 100644 --- a/src/Dist_Archive_Command.php +++ b/src/Dist_Archive_Command.php @@ -122,39 +122,30 @@ public function __invoke( $args, $assoc_args ) { WP_CLI::log( "Replacing $archive_absolute_filepath" . PHP_EOL ); } - chdir( dirname( $source_path ) ); - - $cmd = "zip -r '{$archive_absolute_filepath}' {$archive_output_dir_name}"; + if ( 'zip' === $assoc_args['format'] ) { + // Use PHP's ZipArchive to create zip archives natively, without requiring + // the external zip command, which may not be available on all platforms (e.g. Windows). + if ( $source_path !== $source_dir_path || empty( $file_ignore_rules ) ) { + // Files are already filtered (copied to temp dir) or no filtering needed. + $included_files = null; + } else { + // Apply distignore filtering in place. + $included_files = $this->get_file_list( $source_path ); + } - // If the files are being zipped in place, we need the exclusion rules. - // whereas if they were copied for any reasons above, the rules have already been applied. - if ( $source_path !== $source_dir_path || empty( $file_ignore_rules ) ) { - if ( 'zip' === $assoc_args['format'] ) { - $cmd = "zip -r '{$archive_absolute_filepath}' {$archive_output_dir_name}"; - } elseif ( 'targz' === $assoc_args['format'] ) { - $cmd = "tar -zcvf {$archive_absolute_filepath} {$archive_output_dir_name}"; + if ( true !== $this->create_zip_archive( $archive_absolute_filepath, $source_path, $archive_output_dir_name, $included_files ) ) { + WP_CLI::error( 'Failed to create ZIP archive.' ); } } else { - $tmp_dir = sys_get_temp_dir() . '/' . uniqid( $archive_file_name ); - mkdir( $tmp_dir, 0777, true ); - if ( 'zip' === $assoc_args['format'] ) { - $include_list_filepath = $tmp_dir . '/include-file-list.txt'; - file_put_contents( - $include_list_filepath, - trim( - implode( - "\n", - array_map( - function ( $relative_path ) use ( $source_path ) { - return basename( $source_path ) . $relative_path; - }, - $this->get_file_list( $source_path ) - ) - ) - ) - ); - $cmd = "zip --filesync -r '{$archive_absolute_filepath}' {$archive_output_dir_name} -i@{$include_list_filepath}"; - } elseif ( 'targz' === $assoc_args['format'] ) { + chdir( dirname( $source_path ) ); + + // If the files are being zipped in place, we need the exclusion rules. + // whereas if they were copied for any reasons above, the rules have already been applied. + if ( $source_path !== $source_dir_path || empty( $file_ignore_rules ) ) { + $cmd = "tar -zcvf {$archive_absolute_filepath} {$archive_output_dir_name}"; + } else { + $tmp_dir = sys_get_temp_dir() . '/' . uniqid( $archive_file_name ); + mkdir( $tmp_dir, 0777, true ); $exclude_list_filepath = "{$tmp_dir}/exclude-file-list.txt"; $excludes = array_filter( array_map( @@ -172,25 +163,25 @@ function ( $ignored_file ) use ( $source_path ) { $anchored_flag = ( php_uname( 's' ) === 'Linux' ) ? '--anchored ' : ''; $cmd = "tar {$anchored_flag} --exclude-from={$exclude_list_filepath} -zcvf {$archive_absolute_filepath} {$archive_output_dir_name}"; } - } - $escape_whitelist = 'targz' === $assoc_args['format'] ? array( '^', '*' ) : array(); - WP_CLI::debug( "Running: {$cmd}", 'dist-archive' ); - $escaped_shell_command = $this->escapeshellcmd( $cmd, $escape_whitelist ); + $escape_whitelist = array( '^', '*' ); + WP_CLI::debug( "Running: {$cmd}", 'dist-archive' ); + $escaped_shell_command = $this->escapeshellcmd( $cmd, $escape_whitelist ); - /** - * @var WP_CLI\ProcessRun $ret - */ - $ret = WP_CLI::launch( $escaped_shell_command, false, true ); - if ( 0 === $ret->return_code ) { - $filename = pathinfo( $archive_absolute_filepath, PATHINFO_BASENAME ); - $file_size = $this->get_size_format( (int) filesize( $archive_absolute_filepath ), 2 ); - - WP_CLI::success( "Created {$filename} (Size: {$file_size})" ); - } else { - $error = $ret->stderr ?: $ret->stdout; - WP_CLI::error( $error ); + /** + * @var WP_CLI\ProcessRun $ret + */ + $ret = WP_CLI::launch( $escaped_shell_command, false, true ); + if ( 0 !== $ret->return_code ) { + $error = $ret->stderr ?: $ret->stdout; + WP_CLI::error( $error ); + } } + + $filename = pathinfo( $archive_absolute_filepath, PATHINFO_BASENAME ); + $file_size = $this->get_size_format( (int) filesize( $archive_absolute_filepath ), 2 ); + + WP_CLI::success( "Created {$filename} (Size: {$file_size})" ); } /** @@ -449,6 +440,67 @@ protected function escapeshellcmd( $cmd, $whitelist ) { } + /** + * Create a ZIP archive using PHP's ZipArchive class. + * + * This avoids the need for an external zip command, improving portability + * across platforms including Windows. + * + * @param string $archive_filepath Path to the ZIP archive to create. + * @param string $source_path Path to the directory to archive. + * @param string $archive_output_dir_name The directory name inside the archive. + * @param string[]|null $included_files List of relative file paths to include, + * or null to include all files in $source_path. + * @return bool True on success, false on failure. + */ + private function create_zip_archive( $archive_filepath, $source_path, $archive_output_dir_name, $included_files = null ) { + + if ( file_exists( $archive_filepath ) ) { + unlink( $archive_filepath ); + } + + $zip = new ZipArchive(); + $result = $zip->open( $archive_filepath, ZipArchive::CREATE ); + if ( true !== $result ) { + return false; + } + + if ( null === $included_files ) { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( $source_path, RecursiveDirectoryIterator::SKIP_DOTS ), + RecursiveIteratorIterator::SELF_FIRST + ); + /** + * @var SplFileInfo $file + */ + foreach ( $iterator as $file ) { + $pathname = $file->getPathname(); + $relative_path = substr( $pathname, strlen( $source_path ) ); + $relative_path = str_replace( '\\', '/', $relative_path ); + $archive_path = $archive_output_dir_name . $relative_path; + + if ( $file->isDir() ) { + $zip->addEmptyDir( $archive_path ); + } else { + $zip->addFile( $pathname, $archive_path ); + } + } + } else { + foreach ( $included_files as $relative_filepath ) { + $full_path = $source_path . $relative_filepath; + $archive_path = $archive_output_dir_name . str_replace( '\\', '/', $relative_filepath ); + + if ( is_dir( $full_path ) ) { + $zip->addEmptyDir( $archive_path ); + } else { + $zip->addFile( $full_path, $archive_path ); + } + } + } + + return $zip->close(); + } + /** * Given the path to a directory, check are any of the directories inside it symlinks. * From c236377fb20cab6e1451d7f3296493e0e2040da6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:35:28 +0000 Subject: [PATCH 3/3] Extract add_zip_entry() helper to remove code duplication in create_zip_archive() Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Dist_Archive_Command.php | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/Dist_Archive_Command.php b/src/Dist_Archive_Command.php index 6de8769..c97a399 100644 --- a/src/Dist_Archive_Command.php +++ b/src/Dist_Archive_Command.php @@ -476,31 +476,35 @@ private function create_zip_archive( $archive_filepath, $source_path, $archive_o foreach ( $iterator as $file ) { $pathname = $file->getPathname(); $relative_path = substr( $pathname, strlen( $source_path ) ); - $relative_path = str_replace( '\\', '/', $relative_path ); - $archive_path = $archive_output_dir_name . $relative_path; - - if ( $file->isDir() ) { - $zip->addEmptyDir( $archive_path ); - } else { - $zip->addFile( $pathname, $archive_path ); - } + $this->add_zip_entry( $zip, $pathname, $archive_output_dir_name . str_replace( '\\', '/', $relative_path ), $file->isDir() ); } } else { foreach ( $included_files as $relative_filepath ) { - $full_path = $source_path . $relative_filepath; - $archive_path = $archive_output_dir_name . str_replace( '\\', '/', $relative_filepath ); - - if ( is_dir( $full_path ) ) { - $zip->addEmptyDir( $archive_path ); - } else { - $zip->addFile( $full_path, $archive_path ); - } + $full_path = $source_path . $relative_filepath; + $this->add_zip_entry( $zip, $full_path, $archive_output_dir_name . str_replace( '\\', '/', $relative_filepath ), is_dir( $full_path ) ); } } return $zip->close(); } + /** + * Add a single file or directory entry to an open ZipArchive. + * + * @param ZipArchive $zip The open ZipArchive instance. + * @param string $full_path Absolute filesystem path to the file or directory. + * @param string $archive_path Path to use inside the archive. + * @param bool $is_dir Whether the entry is a directory. + * @return void + */ + private function add_zip_entry( ZipArchive $zip, $full_path, $archive_path, $is_dir ) { + if ( $is_dir ) { + $zip->addEmptyDir( $archive_path ); + } else { + $zip->addFile( $full_path, $archive_path ); + } + } + /** * Given the path to a directory, check are any of the directories inside it symlinks. *