Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2688399
D25033.1734722048.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
16 KB
Referenced Files
None
Subscribers
None
D25033.1734722048.diff
View Options
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -397,6 +397,9 @@
'ArcanistPHPShortTagXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPHPShortTagXHPASTLinterRuleTestCase.php',
'ArcanistPaamayimNekudotayimSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPaamayimNekudotayimSpacingXHPASTLinterRule.php',
'ArcanistPaamayimNekudotayimSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPaamayimNekudotayimSpacingXHPASTLinterRuleTestCase.php',
+ 'ArcanistPackageDescriptor' => 'packages/ArcanistPackageDescriptor.php',
+ 'ArcanistPackagesLoader' => 'packages/ArcanistPackagesLoader.php',
+ 'ArcanistPackagesWorkflow' => 'workflow/ArcanistPackagesWorkflow.php',
'ArcanistParentMemberReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistParentMemberReferenceXHPASTLinterRule.php',
'ArcanistParentMemberReferenceXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistParentMemberReferenceXHPASTLinterRuleTestCase.php',
'ArcanistParenthesesSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistParenthesesSpacingXHPASTLinterRule.php',
@@ -1454,6 +1457,9 @@
'ArcanistPHPShortTagXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistPaamayimNekudotayimSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistPaamayimNekudotayimSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
+ 'ArcanistPackageDescriptor' => 'Phobject',
+ 'ArcanistPackagesLoader' => 'Phobject',
+ 'ArcanistPackagesWorkflow' => 'ArcanistWorkflow',
'ArcanistParentMemberReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistParentMemberReferenceXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistParenthesesSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
diff --git a/src/packages/ArcanistPackageDescriptor.php b/src/packages/ArcanistPackageDescriptor.php
new file mode 100644
--- /dev/null
+++ b/src/packages/ArcanistPackageDescriptor.php
@@ -0,0 +1,98 @@
+<?php
+
+final class ArcanistPackageDescriptor
+ extends Phobject {
+
+ private $identifier;
+ private $version;
+ private $configSource;
+ private $description;
+
+ public static function fromConfig(
+ array $config_item,
+ ArcanistConfigurationSource $config_source) {
+
+ $ident = idx($config_item, 'identifier');
+ $version = idx($config_item, 'version');
+ if (!strlen($ident) || !strlen($version)) {
+ throw new ArcanistUsageException(
+ pht(
+ 'Package configuration requires both identifier and a version. '.
+ '(reading %s)',
+ $config_source->getSourceDisplayName()));
+ }
+
+ return id(new self())
+ ->setIdentifier($ident)
+ ->setVersion($version)
+ ->setConfigSource($config_source);
+ }
+
+ public static function fromDirectory($directory) {
+ $filename = $directory.'/.arcpackage';
+ if (!Filesystem::pathExists($filename)) {
+ return null;
+ }
+ $manifest = phutil_json_decode(Filesystem::readFile($filename));
+ return self::fromManifest($manifest);
+ }
+
+ /**
+ * "Manifest" refers to the .arcpackage file.
+ */
+ public static function fromManifest(array $manifest) {
+ $ident = idx($manifest, 'identifier');
+ $version = idx($manifest, 'version');
+ if (!strlen($ident) || !strlen($version)) {
+ throw new ArcanistUsageException(pht('Bad manifest file!'));
+ }
+ return id(new self())
+ ->setIdentifier($ident)
+ ->setVersion($version)
+ ->setDescription(idx($manifest, 'description'));
+ }
+
+ public function __toString() {
+ return pht('Package %s (ver %s)', $this->identifier, $this->version);
+ }
+
+ public function getDirectoryName() {
+ return $this->identifier.'-'.$this->version;
+ }
+
+ public function setConfigSource(ArcanistConfigurationSource $config_source) {
+ $this->configSource = $config_source;
+ return $this;
+ }
+
+ public function getConfigSource() {
+ return $this->configSource;
+ }
+
+ public function setDescription($description) {
+ $this->description = $description;
+ return $this;
+ }
+
+ public function getDescription() {
+ return $this->description;
+ }
+
+ public function setIdentifier(string $identifier) {
+ $this->identifier = $identifier;
+ return $this;
+ }
+
+ public function getIdentifier() {
+ return $this->identifier;
+ }
+
+ public function setVersion(string $version) {
+ $this->version = $version;
+ return $this;
+ }
+
+ public function getVersion() {
+ return $this->version;
+ }
+}
diff --git a/src/packages/ArcanistPackagesLoader.php b/src/packages/ArcanistPackagesLoader.php
new file mode 100644
--- /dev/null
+++ b/src/packages/ArcanistPackagesLoader.php
@@ -0,0 +1,152 @@
+<?php
+
+final class ArcanistPackagesLoader
+ extends Phobject {
+
+ private $configurationSourceList;
+
+ public function setConfigurationSourceList(
+ ArcanistConfigurationSourceList $sources) {
+
+ $this->configurationSourceList = $sources;
+ return $this;
+ }
+
+ public function loadPackages() {
+ $log = new ArcanistLogEngine();
+ $requested = $this->listRequestedPackages();
+
+ $this->checkForConflicts($requested);
+
+ $missing = array();
+
+ foreach ($requested as $package) {
+ $package_dir = $this->resolvePath($package);
+ $found = ArcanistPackageDescriptor::fromDirectory($package_dir);
+ if ($found != null) {
+ if ($found->getIdentifier() != $package->getIdentifier() ||
+ $found->getVersion() != $package->getVersion()) {
+ $log->writeError(
+ pht('PACKAGE LOAD ERROR'),
+ pht(
+ 'Expected to find package %s in location %s, found %s instead',
+ $package->getIdentifier(),
+ $package_dir,
+ $found->getIdentifier()));
+ $missing[] = $package;
+ continue;
+ }
+
+ try {
+ $this->loadPackage($found);
+ } catch (PhutilBootloaderException $ex) {
+ $log->writeError(
+ pht('LOAD ERROR'),
+ pht(
+ 'Failed to load package "%s". This package is '.
+ 'specified by "%s"',
+ $package->getIdentifier(),
+ $package->getConfigurationSource()->getSourceDisplayName()));
+
+ $prompt = pht('Continue loading without this package?');
+ if (!phutil_console_confirm($prompt)) {
+ throw $ex;
+ }
+ }
+ } else {
+ $missing[] = $package;
+ }
+ }
+
+ if ($missing) {
+ $log->writeError(
+ pht('MISSING PACKAGES'),
+ pht(
+ 'The following packages were requested but not found: %s.',
+ implode(', ', $missing)));
+
+ // TODO try to install; use `prompts`?
+ $prompt = pht('Continue without these packages?');
+ if (!phutil_console_confirm($prompt)) {
+ throw new ArcanistUserAbortException('Requested package not found.');
+ }
+ }
+ }
+
+ private function checkForConflicts($requested) {
+ $log = new ArcanistLogEngine();
+ $packages = mgroup($requested, 'getIdentifier', 'getVersion');
+
+ foreach ($packages as $identifier => $versions) {
+ if (count($versions) > 1) {
+ $requests = array();
+ foreach ($versions as $version => $list) {
+ $package = head($list);
+ $requests[] = pht(
+ 'v%s - Requested from %s',
+ $package->getVersion(),
+ $package->getConfigSource()->getSourceDisplayName());
+ }
+
+ $log->writeError(
+ pht('CONFLICTING PACKAGE REQUESTED'),
+ pht(
+ 'The package "%s" has conflicting requirements: %s.',
+ $identifier,
+ implode('; ', $requests)));
+
+ throw new ArcanistUsageException(
+ pht('Conflicting requests for package %s', $identifier));
+ }
+ }
+ }
+
+ private function loadPackage(ArcanistPackageDescriptor $package) {
+ phutil_load_library($this->resolvePath($package).'/src');
+ }
+
+ public function listRequestedPackages() {
+ $requested = array();
+ $items = $this->configurationSourceList->getStorageValueList('packages');
+
+ foreach ($items as $item) {
+ foreach ($item->getValue() as $package_item) {
+ $requested[] = ArcanistPackageDescriptor::fromConfig(
+ $package_item,
+ $item->getConfigurationSource());
+ }
+ }
+ return $requested;
+ }
+
+ /**
+ * This method may be slow, because it looks for and loads lots of files.
+ */
+ public function listInstalledPackages() {
+ $packages_root = $this->getPackagesDirectory();
+
+ if (!Filesystem::pathExists($packages_root)) {
+ return array();
+ }
+
+ $dirs = Filesystem::listDirectory($packages_root);
+
+ $packages = array();
+ foreach ($dirs as $dir) {
+ $packages[$dir] =
+ ArcanistPackageDescriptor::fromDirectory($packages_root.'/'.$dir);
+ }
+ return array_filter($packages);
+ }
+
+ private function resolvePath(ArcanistPackageDescriptor $package) {
+ return $this->getPackagesDirectory().'/'.$package->getDirectoryName();
+ }
+
+ private function getPackagesDirectory() {
+ if (phutil_is_windows()) {
+ return getenv('APPDATA').'/.cache/arcanist-packages';
+ }
+ return getenv('HOME').'/.cache/arcanist-packages';
+ }
+}
diff --git a/src/runtime/ArcanistRuntime.php b/src/runtime/ArcanistRuntime.php
--- a/src/runtime/ArcanistRuntime.php
+++ b/src/runtime/ArcanistRuntime.php
@@ -102,6 +102,7 @@
$config = $config_engine->newConfigurationSourceList();
$this->loadLibraries($config_engine, $config, $args);
+ $this->loadPackages($config_engine, $config, $args);
// Now that we've loaded libraries, we can validate configuration.
// Do this before continuing since configuration can impact other
@@ -371,11 +372,11 @@
break;
}
- $this->loadLibrary($engine, $library_source, $description);
+ $this->loadLegacyLibrary($engine, $library_source, $description);
}
}
- private function loadLibrary(
+ private function loadLegacyLibrary(
ArcanistConfigurationEngine $engine,
$location,
$description) {
@@ -512,6 +513,16 @@
}
}
+ private function loadPackages(
+ ArcanistConfigurationEngine $engine,
+ ArcanistConfigurationSourceList $config,
+ PhutilArgumentParser $args) {
+
+ $loader = id(new ArcanistPackagesLoader())
+ ->setConfigurationSourceList($config);
+ $loader->loadPackages();
+ }
+
private function newToolset(array $argv) {
$binary = basename($argv[0]);
diff --git a/src/workflow/ArcanistPackagesWorkflow.php b/src/workflow/ArcanistPackagesWorkflow.php
new file mode 100644
--- /dev/null
+++ b/src/workflow/ArcanistPackagesWorkflow.php
@@ -0,0 +1,193 @@
+<?php
+
+final class ArcanistPackagesWorkflow extends ArcanistWorkflow {
+
+ public function supportsToolset(ArcanistToolset $toolset) {
+ return true;
+ }
+
+ public function getWorkflowName() {
+ return 'packages';
+ }
+
+ public function getWorkflowInformation() {
+ $help = pht(<<<EOTEXT
+Supports: cli
+When used with no arguments, lists available and configured packages.
+
+**WARNING**: this workflow is highly unstable. Just ignore it for now.
+EOTEXT
+);
+ return $this->newWorkflowInformation()
+ ->setSynopsis(pht('Manage Extension Packages'))
+ ->addExample(pht('**packages**'))
+ ->addExample(pht('**packages** --install-from-tgz __file__'))
+ ->setHelp($help);
+ }
+
+ public function getWorkflowArguments() {
+ return array(
+ $this->newWorkflowArgument('install-from-tgz')
+ ->setParameter('package_archive')
+ ->setHelp(pht('UNSTABLE: Install package from an archive')),
+ );
+ }
+
+ public function runWorkflow() {
+ $fromtar = $this->getArgument('install-from-tgz');
+ if ($fromtar !== null) {
+ $this->runInstallFromTar($fromtar);
+ return;
+ }
+
+ $this->runListPackagesWorkflow();
+ }
+
+ private function runInstallFromTar($tar_file) {
+ Filesystem::assertReadable($tar_file);
+ Filesystem::assertIsFile($tar_file);
+
+ list($stdout, $stderr) = execx(
+ 'tar -x -z -O -f %s ./.arcpackage',
+ $tar_file);
+ $manifest = phutil_json_decode($stdout);
+ $descriptor = ArcanistPackageDescriptor::fromManifest($manifest);
+
+ $dir = $this->resolvePath($descriptor);
+ if (Filesystem::pathExists($dir)) {
+ throw new ArcanistUsageException(
+ pht('this package already installed at %s', $dir));
+ }
+
+ Filesystem::createDirectory($dir, 0755, true);
+ execx('tar -x -z -f %s -C %s', $tar_file, $dir);
+ }
+
+ private function runListPackagesWorkflow() {
+ $console = PhutilConsole::getConsole();
+
+ $loader = id(new ArcanistPackagesLoader())
+ ->setConfigurationSourceList($this->getConfigurationSourceList());
+
+ $configured = $loader->listRequestedPackages();
+ $configured = mpull($configured, null, 'getDirectoryName');
+
+ $installed = $loader->listInstalledPackages();
+ $installed = mpull($installed, null, 'getDirectoryName');
+
+ $report = array();
+
+ foreach ($configured as $key => $descriptor) {
+ $report[$key] = array(
+ 'descriptor' => $descriptor,
+ 'status' => 'requested',
+ );
+ }
+
+ foreach ($installed as $key => $descriptor) {
+ if (array_key_exists($key, $report)) {
+ $report[$key]['status'] = 'loaded';
+ $report[$key]['descriptor']->setDescription(
+ $descriptor->getDescription());
+ } else {
+ $report[$key] = array(
+ 'descriptor' => $descriptor,
+ 'status' => 'installed',
+ );
+ }
+ }
+
+ $color_map = array(
+ 'loaded' => 'green',
+ 'installed' => 'yellow',
+ 'requested' => 'red',
+ );
+ $text_map = $this->getPackageStatusMap();
+
+ ksort($report);
+
+ foreach ($report as $ident => $package) {
+ $status = $package['status'];
+ $color = $color_map[$status];
+ $text = $text_map[$status];
+
+ $descriptor = $package['descriptor'];
+
+ $description = $descriptor->getDescription();
+ if (!strlen($description)) {
+ $description = pht('Mystery package');
+ }
+ $config_text = null;
+ $config_source = $descriptor->getConfigSource();
+ if ($config_source) {
+ $config_text = pht(
+ '(Requested at %s)',
+ $config_source->getSourceDisplayName());
+ }
+ $console->writeOut(
+ "<bg:".$color.">** %s **</bg> **%s** %s (%s) %s\n",
+ $text,
+ $descriptor->getIdentifier(),
+ $descriptor->getVersion(),
+ $description,
+ $config_text);
+ }
+ }
+
+ private function loadConfiguredPackages() {
+ $items = $this->getConfigurationSourceList()
+ ->getStorageValueList('packages');
+
+ $packages = array();
+ foreach ($items as $item) {
+ foreach ($item->getValue() as $package_item) {
+ $packages[] = $package_item + array(
+ 'config-source' => $item->getConfigurationSource(),
+ );
+ }
+ }
+ return $packages;
+ }
+
+ /**
+ * Get human-readable statuses, padded to fixed width.
+ *
+ * @return map<string, string> Human-readable status names.
+ */
+ // TODO this function is copied from Linters workflow
+ private function getPackageStatusMap() {
+ $text_map = array(
+ 'loaded' => pht('LOADED'),
+ 'installed' => pht('AVAILABLE'),
+ 'requested' => pht('MISSING'),
+ );
+
+ $sizes = array();
+ foreach ($text_map as $key => $string) {
+ $sizes[$key] = phutil_utf8_console_strlen($string);
+ }
+
+ $longest = max($sizes);
+ foreach ($text_map as $key => $string) {
+ if ($sizes[$key] < $longest) {
+ $text_map[$key] .= str_repeat(' ', $longest - $sizes[$key]);
+ }
+ }
+
+ $text_map['padding'] = str_repeat(' ', $longest);
+
+ return $text_map;
+ }
+
+ // TODO these 2 are copy-pasted from ArcanistPackagesLoader
+ private function resolvePath(ArcanistPackageDescriptor $package) {
+ return $this->getPackagesDirectory().'/'.$package->getDirectoryName();
+ }
+
+ private function getPackagesDirectory() {
+ if (phutil_is_windows()) {
+ return getenv('APPDATA').'/.cache/arcanist-packages';
+ }
+ return getenv('HOME').'/.cache/arcanist-packages';
+ }
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Dec 20, 19:14 (13 h, 32 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1015630
Default Alt Text
D25033.1734722048.diff (16 KB)
Attached To
Mode
D25033: Packages: Load'em from .cache
Attached
Detach File
Event Timeline
Log In to Comment