8000 (dev/core#5700) `cv dl` - Split download substeps. Fix upgrades of EFv1=>EFv2. by totten · Pull Request #247 · civicrm/cv · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

(dev/core#5700) cv dl - Split download substeps. Fix upgrades of EFv1=>EFv2. #247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bin/cv
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#!/usr/bin/env php
<?php
define('CV_BIN', __FILE__);
putenv('BOX_REQUIREMENT_CHECKER=0');
// ^^ If we make a recursive call to `cv.phar -vvv`, then do NOT reprint the box requirements-check.

ini_set('display_errors', 'stderr');
if (PHP_SAPI !== 'cli') {
printf("cv is a command-line tool. It is designed to run with PHP_SAPI \"%s\". The active PHP_SAPI is \"%s\".\n", 'cli', PHP_SAPI);
Expand Down
93 changes: 93 additions & 0 deletions lib/src/Top.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace Civi\Cv;

class Top {

/**
* @var string|null
*/
private static $prefixRegex;

/**
* Call a top-level function or method
*
* @param string|string[] $callable
* Ex: 'drupal_bootstrap'
* Ex: '\drupal_bootstrap'
* Ex: ['Drupal', 'service']
* Note: All symbols are implicitly relative to top-level namespace.
* @param mixed[] $args
* @return mixed
*/
public static function call($callable, ...$args) {
$callable = static::symbol($callable);
return call_user_func_array($callable, $args);
}

/**
* Instantiate an object (based on the top-level classname).
*
* @param string $class
* Ex: 'Symfony\Contracts\EventDispatcher\EventDispatcher'
* Ex: '\Symfony\Contracts\EventDispatcher\EventDispatcher'
* Note: All symbols are implicitly relative to top-level namespace.
* @param mixed[] $args
* @return object
*/
public static function create($class, ...$args) {
$class = static::symbol($class);
return new $class(...$args);
}

/**
* Evaluate a symbol, returning the top-level version of that symbol.
*
* @param string|string[] $symbol
* Ex: 'Drupal', '\Symfony', ['\Drupal', 'service'], 'Drupal::service'
* @return string|string[]
* The same symbol, ut without any php-scoper prefixes.
*/
public static function symbol($symbol) {
if (is_string($symbol)) {
// Translate function or class
if (static::$prefixRegex === NULL) {
static::$prefixRegex = static::createPrefixRegex();
}
$result = preg_replace(static::$prefixRegex, '\\', $symbol);
if ($result[0] !== '\\') {
$result = '\\' . $result;
}
return $result;
}
elseif (is_array($symbol)) {
// Translate class
$symbol[0] = static::symbol($symbol[0]);
return $symbol;
}
else {
return $symbol;
}
}

/**
* Build a string to match the php-scoper prefix.
*
* @return string
*/
protected static function createPrefixRegex(): string {
$parts = explode('\\', \Path\To\Dummy::class);
$topNS = $parts[0];

// Are we running in a scoped PHAR?
if ($topNS[0] === '_' || substr($topNS, -4) === 'Phar' || substr($topNS, -4) === 'phar') {
// In `box`+`php-scoper`, default prefix is dynamic value like '_HumbugXYZ'.
// Most of my projects use an explicit prefix like 'MyAppPhar'
return ';^\\\?' . preg_quote($parts[0], ';') . '\\\;';
}
else {
return ';^\\\;';
}
}

}
1 change: 1 addition & 0 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public function createCommands($context = 'default') {
$commands[] = new \Civi\Cv\Command\CoreCheckReqCommand();
$commands[] = new \Civi\Cv\Command\CoreInstallCommand();
$commands[] = new \Civi\Cv\Command\CoreUninstallCommand();
$commands[] = new \Civi\Cv\Command\QueueNextCommand();
$commands[] = new \Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand();
}
return $commands;
Expand Down
177 changes: 150 additions & 27 deletions src/Command/ExtensionDownloadCommand.php
F438
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<?php
namespace Civi\Cv\Command;

use Civi\Cv\Cv;
use Civi\Cv\Exception\QueueTaskException;
use Civi\Cv\ExtensionPolyfill\PfQueueDownloader;
use Civi\Cv\Util\ConsoleSubprocessQueueRunner;
use Civi\Cv\Util\ExtensionTrait;
use Civi\Cv\Util\Filesystem;
use Civi\Cv\Util\HeadlessDownloader;
Expand Down Expand Up @@ -33,6 +37,8 @@ protected function configure() {
->addOption('force', 'f', InputOption::VALUE_NONE, 'If an extension already exists, download it anyway.')
->addOption('to', NULL, InputOption::VALUE_OPTIONAL, 'Download to a specific directory (absolute path).')
->addOption('keep', 'k', InputOption::VALUE_NONE, 'If an extension already exists, keep it.')
->addOption('dry-run', NULL, InputOption::VALUE_NONE, 'Preview the list of tasks')
->addOption('step', NULL, InputOption::VALUE_NONE, 'Run the tasks in steps, pausing before each step')
->addArgument('key-or-name', InputArgument::IS_ARRAY, 'One or more extensions to enable. Identify the extension by full key ("org.example.foobar") or short name ("foobar"). Optionally append a URL.')
->setHelp('Download and enable an extension

Expand Down Expand Up @@ -67,6 +73,9 @@ protected function initialize(InputInterface $input, OutputInterface $output) {
if ($input->hasOption('bare') && $input->getOption('bare')) {
$input->setOption('level', 'none');
$input->setOption('no-install', TRUE);
if (empty($input->getOption('to'))) {
throw new \LogicException("If --bare is specified, then --to must also be specified.");
}
}
if ($extRepoUrl = $this->parseRepoUrl($input)) {
global $civicrm_setting;
Expand All @@ -76,6 +85,12 @@ protected function initialize(InputInterface $input, OutputInterface $output) {
}

protected function execute(InputInterface $input, OutputInterface $output): int {
if ($input->getOption('step')) {
if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERY_VERBOSE) {
$output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE);
}
}

$fs = new Filesystem();

if ($input->getOption('to') && !$fs->isAbsolutePath($input->getOption('to'))) {
Expand All @@ -100,7 +115,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
}

[$downloads, $errors] = $this->parseDownloads($input);
[$requestedDownloads, $errors] = $this->parseDownloads($input);
if ($refresh == 'auto' && !empty($errors)) {
$output->writeln("<info>Extension cache does not contain requested item(s)</info>");
$refresh = 'yes';
Expand All @@ -115,59 +130,167 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$output->getErrorOutput()->writeln("<error>$error</error>");
}
$output->getErrorOutput()->writeln("<comment>Tip: To customize the feed, review options in \"cv {$input->getFirstArgument()} --help\"");
$output->getErrorOutput()->writeln("<comment>Tip: To browse available downloads, run \"cv ext:list -R\"</comment>");
$output->getErrorOutput()->writeln("<comment>Tip: To browse available requestedDownloads, run \"cv ext:list -R\"</comment>");
return 1;
}

if ($input->getOption('to') && count($downloads) > 1) {
if ($input->getOption('to') && count($requestedDownloads) > 1) {
throw new \RuntimeException("When specifying --to, you can only download one extension at a time.");
}
elseif (empty($requestedDownloads)) {
Cv::output()->writeln('Nothing to do.');
return 0;
}
elseif ($input->getOption('bare')) {
return $this->executeWithBare($requestedDownloads);
}
else {
return $this->executeWithQueue($requestedDownloads);
}
}

foreach ($downloads as $key => $url) {
$action = $this->pickAction($input, $output, $key);
/**
* In a bare download, we don't have access to a copy of CiviCRM. This is useful
* if you want to grab a specific extension from a feed (before Civi is installed).
*
* Ex: cv dl -b "@https://civicrm.org/extdir/ver=$DM_VERSION/$SOME_EXT.xml" --to="$SOME_FOLDER"
*
* @param arr 9E7A ay $requestedDownloads
* @return int
*/
protected function executeWithBare(array $requestedDownloads): int {
foreach ($requestedDownloads as $key => $url) {
$action = $this->pickAction(Cv::input(), Cv::output(), $key);
switch ($action) {
case 'download':
if ($to = $input->getOption('to')) {
$output->writeln("<info>Downloading extension \"$key\" ($url) to \"$to\"</info>");
if ($to = Cv::input()->getOption('to')) {
Cv::output()->writeln("<info>Downloading extension \"$key\" ($url) to \"$to\"</info>");
$dl = new HeadlessDownloader();
$dl->run($url, $key, $input->getOption('to'), TRUE);
$dl->run($url, $key, Cv::input()->getOption('to'), TRUE);
}
else {
$output->writeln("<info>Downloading extension \"$key\" ($url)</info>");
$this->assertBooted();
$result = VerboseApi::callApi3Success('Extension', 'download', array(
'key' => $key,
'url' => $url,
'install' => !$input->getOption('no-install'),
));
throw new \LogicException("Missing option --to");
}
break;

case 'abort':
Cv::output()->writeln("<error>Aborted</error>");
return 1;

case 'install':
case 'skip':
Cv::output()->writeln("<comment>Skipped extension \"$key\".</comment>");
break;

default:
throw new \RuntimeException("Unrecognized action: $action");
}
}

return 0;
}

/**
* Perform a normal download. This may involve several steps, such as fetching a ZIP,
* extracting it, putting it inplace (replacing an old ext), flushing caches,
* running upgrades, etc.
*
* This runs in a queue, and each queue item is launched as a separate subprocess.
*
* @param array $requestedDownloads
* @return int
*/
protected function executeWithQueue(array $requestedDownloads): int {
$this->assertBooted();

if (Cv::input()->getOption('to')) {
// FIXME
throw new \RuntimeException('Queued downloader does not currently support --to');
}

$queueSpec = $this->createQueueSpec();
$newQueueSpec = array_merge($queueSpec, ['reset' => TRUE]);
$reloadQueueSpec = array_merge($queueSpec, ['reset' => FALSE]);
$queue = \CRM_Queue_Service::singleton()->create($newQueueSpec);

if (class_exists('CRM_Extension_QueueDownloader') && class_exists('CRM_Extension_QueueTasks')) {
// Note: 6.1.0 has QueueDownloader but an insufficient signature. Presence of QueueTasks correlates with revised signature (6.1.1-ish).
$downloader = new \CRM_Extension_QueueDownloader(TRUE, $queue);
$runner = new ConsoleSubprocessQueueRunner(Cv::io(), $reloadQueueSpec, Cv::input()->getOption('dry-run'), Cv::input()->getOption('step'));
}
else {
$downloader = new PfQueueDownloader(TRUE, $queue);
$runner = new ConsoleSubprocessQueueRunner(Cv::io(), $reloadQueueSpec, Cv::input()->getOption('dry-run'), Cv::input()->getOption('step'));
}

$batch = NULL;
$updateBatch = function(?string $method, array $data) use (&$batch, $downloader) {
if ($batch === NULL) {
$batch = ['method' => $method, 'data' => $data];
}
elseif ($batch['method'] === $method) {
$batch['data'] = array_merge($batch['data'], $data);
}
else {
if ($batch['method'] === 'addDownloads') {
$downloader->addDownloads($batch['data'], !Cv::input()->getOption('no-install'));
}
elseif ($batch['method'] === 'addEnable') {
$downloader->addEnable($batch['data']);
}
else {
throw new \LogicException("Unrecognized method: $method");
}
$batch = ['method' => $method, 'data' => $data];
}
};

foreach ($requestedDownloads as $key => $url) {
$action = $this->pickAction(Cv::input(), Cv::output(), $key);
switch ($action) {
case 'download':
$updateBatch('addDownloads', [$key => $url]);
break;

case 'install':
$output->writeln("<info>Found extension \"$key\". Enabling.</info>");
$result = VerboseApi::callApi3Success('Extension', 'enable', array(
'key' => $key,
));
$updateBatch('addEnable', [$key]);
break;

case 'abort':
$output->writeln("<error>Aborted</error>");
Cv::output()->writeln("<error>Aborted</error>");
return 1;

case 'skip':
$output->writeln("<comment>Skipped extension \"$key\".</comment>");
Cv::output()->writeln("<comment>Skipped extension \"$key\".</comment>");
break;

default:
throw new \RuntimeException("Unrecognized action: $action");
}
}

if (!empty($result['is_error'])) {
return 1;
}
$updateBatch(NULL, []);
$downloader->fillQueue();
try {
$runner->runAll();
return 0;
}
catch (QueueTaskException $e) {
// The earlier handlers will output details.
return 1;
}
}

return 0;
protected function createQueueSpec(): array {
return [
'name' => 'cli-ext-dl',
'type' => 'Sql',
'runner' => 'task',
'is_autorun' => FALSE,
'retry_limit' => 0,
'error' => 'abort',
'is_persistent' => FALSE,
];
}

/**
Expand Down Expand Up @@ -318,7 +441,7 @@ protected function pickAction(
return 'download';
}
elseif ($input->getOption('keep')) {
return $input->getOptions('no-install') ? 'skip' : 'install';
return $input->getOption('no-install') ? 'skip' : 'install';
}
elseif ($input->getOption('force')) {
return 'download';
Expand All @@ -339,7 +462,7 @@ protected function pickAction(
return 'download';

case 'k':
return $input->getOptions('no-install') ? 'skip' : 'install';
return $input->getOption('no-install') ? 'skip' : 'install';

case 'a':
default:
Expand Down
Loading
0