Page MenuHomePhorge

D25033.1734695869.diff
No OneTemporary

D25033.1734695869.diff

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

Mime Type
text/plain
Expires
Fri, Dec 20, 11:57 (5 h, 42 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1015630
Default Alt Text
D25033.1734695869.diff (16 KB)

Event Timeline