mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-29 02:02:40 +01:00
Allow global config to load libraries and set test engines
Summary: Khan Academy is looking into lint configuration, but doesn't use ".arcconfig" because they have a large number of repositories. Making configuration more flexible generally gives us more options for onboarding installs. - Currently, only project config (".arcconfig") can load libraries. Allow user config ("~/.arcrc") to load libraries as well. - Currently, only project config can set lint/unit engines. Allow user config to set default lint/unit engines. - Add some type checking to "arc set-config". - Add "arc set-config --show". Test Plan: - **load** - Ran `arc set-config load xxx`, got error about format. - Ran `arc set-config load ["apple"]`, got warning on running 'arc' commands (no such library) but was able to run 'arc set-config' again to clear it. - Ran `arc set-config load ["/path/to/a/lib/src/"]`, worked. - Ran `arc list --trace`, verified my library loaded in addition to `.arcconfig` libraries. - Ran `arc list --load-phutil-library=xxx --trace`, verified only that library loaded. - Ran `arc list --trace --load-phutil-library=apple --trace`, got hard error about bad library. - Set `.arcconfig` to point at a bad library, verified hard error. - **lint.engine** / **unit.engine** - Removed lint engine from `.arcconfig`, ran "arc lint", got a run with specified engine. - Removed unit engine from `.arcconfig`, ran "arc unit", got a run with specified engine. - **--show** - Ran `arc set-config --show`. - **misc** - Ran `arc get-config`. Reviewers: csilvers, btrahan, vrana Reviewed By: csilvers CC: aran Differential Revision: https://secure.phabricator.com/D2618
This commit is contained in:
parent
e214ea7b18
commit
8b7a5157f8
7 changed files with 288 additions and 82 deletions
|
@ -59,87 +59,46 @@ try {
|
||||||
throw new ArcanistUsageException("No command provided. Try 'arc help'.");
|
throw new ArcanistUsageException("No command provided. Try 'arc help'.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$global_config = ArcanistBaseWorkflow::readGlobalArcConfig();
|
||||||
$working_copy = ArcanistWorkingCopyIdentity::newFromPath($working_directory);
|
$working_copy = ArcanistWorkingCopyIdentity::newFromPath($working_directory);
|
||||||
|
|
||||||
|
// Load additional libraries, which can provide new classes like configuration
|
||||||
|
// overrides, linters and lint engines, unit test engines, etc.
|
||||||
|
|
||||||
|
// If the user specified "--load-phutil-library" one or more times from
|
||||||
|
// the command line, we load those libraries **instead** of whatever else
|
||||||
|
// is configured. This is basically a debugging feature to let you force
|
||||||
|
// specific libraries to load regardless of the state of the world.
|
||||||
if ($load) {
|
if ($load) {
|
||||||
$libs = $load;
|
// Load the flag libraries. These must load, since the user specified them
|
||||||
|
// explicitly.
|
||||||
|
arcanist_load_libraries(
|
||||||
|
$load,
|
||||||
|
$must_load = true,
|
||||||
|
$lib_source = 'a "--load-phutil-library" flag',
|
||||||
|
$working_copy,
|
||||||
|
$config_trace_mode);
|
||||||
} else {
|
} else {
|
||||||
$libs = $working_copy->getConfig('phutil_libraries');
|
// Load libraries in global 'load' config, as per "arc set-config load". We
|
||||||
}
|
// need to fail softly if these break because errors would prevent the user
|
||||||
if ($libs) {
|
// from running "arc set-config" to correct them.
|
||||||
foreach ($libs as $name => $location) {
|
arcanist_load_libraries(
|
||||||
|
idx($global_config, 'load', array()),
|
||||||
|
$must_load = false,
|
||||||
|
$lib_source = 'the "load" setting in global config',
|
||||||
|
$working_copy,
|
||||||
|
$config_trace_mode);
|
||||||
|
|
||||||
// Try to resolve the library location. We look in several places, in
|
// Load libraries in ".arcconfig". Libraries here must load.
|
||||||
// order:
|
arcanist_load_libraries(
|
||||||
//
|
$working_copy->getConfig('phutil_libraries'),
|
||||||
// 1. Inside the working copy. This is for phutil libraries within the
|
$must_load = true,
|
||||||
// project. For instance "library/src" will resolve to
|
$lib_source = 'the "phutil_libraries" setting in ".arcconfig"',
|
||||||
// "./library/src" if it exists.
|
$working_copy,
|
||||||
// 2. In the same directory as the working copy. This allows you to
|
$config_trace_mode);
|
||||||
// check out a library alongside a working copy and reference it.
|
|
||||||
// If we haven't resolved yet, "library/src" will try to resolve to
|
|
||||||
// "../library/src" if it exists.
|
|
||||||
// 3. Using normal libphutil resolution rules. Generally, this means
|
|
||||||
// that it checks for libraries next to libphutil, then libraries
|
|
||||||
// in the PHP include_path.
|
|
||||||
|
|
||||||
$resolved = false;
|
|
||||||
|
|
||||||
// Check inside the working copy.
|
|
||||||
$resolved_location = Filesystem::resolvePath(
|
|
||||||
$location,
|
|
||||||
$working_copy->getProjectRoot());
|
|
||||||
if (Filesystem::pathExists($resolved_location)) {
|
|
||||||
$location = $resolved_location;
|
|
||||||
$resolved = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we didn't find anything, check alongside the working copy.
|
|
||||||
if (!$resolved) {
|
|
||||||
$resolved_location = Filesystem::resolvePath(
|
|
||||||
$location,
|
|
||||||
dirname($working_copy->getProjectRoot()));
|
|
||||||
if (Filesystem::pathExists($resolved_location)) {
|
|
||||||
$location = $resolved_location;
|
|
||||||
$resolved = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($config_trace_mode) {
|
|
||||||
echo "Loading phutil library '{$name}' from '{$location}'...\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
phutil_load_library($location);
|
|
||||||
} catch (PhutilBootloaderException $ex) {
|
|
||||||
$error_msg = sprintf(
|
|
||||||
'Failed to load library "%s" at location "%s". Please check the '.
|
|
||||||
'"phutil_libraries" setting in your .arcconfig file. Refer to '.
|
|
||||||
'<http://www.phabricator.com/docs/phabricator/article/'.
|
|
||||||
'Arcanist_User_Guide_Configuring_a_New_Project.html> '.
|
|
||||||
'for more information.',
|
|
||||||
$name,
|
|
||||||
$location);
|
|
||||||
throw new ArcanistUsageException($error_msg);
|
|
||||||
} catch (PhutilLibraryConflictException $ex) {
|
|
||||||
if ($ex->getLibrary() != 'arcanist') {
|
|
||||||
throw $ex;
|
|
||||||
}
|
|
||||||
|
|
||||||
$arc_dir = dirname(dirname(__FILE__));
|
|
||||||
$error_msg =
|
|
||||||
"You are trying to run one copy of Arcanist on another copy of ".
|
|
||||||
"Arcanist. This operation is not supported. To execute Arcanist ".
|
|
||||||
"operations against this working copy, run './bin/arc' (from the ".
|
|
||||||
"current working copy) not some other copy of 'arc' (you ran one ".
|
|
||||||
"from '{$arc_dir}').";
|
|
||||||
|
|
||||||
throw new ArcanistUsageException($error_msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$user_config = ArcanistBaseWorkflow::readUserConfigurationFile();
|
$user_config = ArcanistBaseWorkflow::readUserConfigurationFile();
|
||||||
$global_config = ArcanistBaseWorkflow::readGlobalArcConfig();
|
|
||||||
|
|
||||||
$config = $working_copy->getConfig('arcanist_configuration');
|
$config = $working_copy->getConfig('arcanist_configuration');
|
||||||
if ($config) {
|
if ($config) {
|
||||||
|
@ -438,3 +397,91 @@ function die_with_bad_php($message) {
|
||||||
echo "\n\n";
|
echo "\n\n";
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function arcanist_load_libraries(
|
||||||
|
$load,
|
||||||
|
$must_load,
|
||||||
|
$lib_source,
|
||||||
|
ArcanistWorkingCopyIdentity $working_copy,
|
||||||
|
$config_trace_mode) {
|
||||||
|
|
||||||
|
if (!$load) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($load as $location) {
|
||||||
|
|
||||||
|
// Try to resolve the library location. We look in several places, in
|
||||||
|
// order:
|
||||||
|
//
|
||||||
|
// 1. Inside the working copy. This is for phutil libraries within the
|
||||||
|
// project. For instance "library/src" will resolve to
|
||||||
|
// "./library/src" if it exists.
|
||||||
|
// 2. In the same directory as the working copy. This allows you to
|
||||||
|
// check out a library alongside a working copy and reference it.
|
||||||
|
// If we haven't resolved yet, "library/src" will try to resolve to
|
||||||
|
// "../library/src" if it exists.
|
||||||
|
// 3. Using normal libphutil resolution rules. Generally, this means
|
||||||
|
// that it checks for libraries next to libphutil, then libraries
|
||||||
|
// in the PHP include_path.
|
||||||
|
//
|
||||||
|
// Note that absolute paths will just resolve absolutely through rule (1).
|
||||||
|
|
||||||
|
$resolved = false;
|
||||||
|
|
||||||
|
// Check inside the working copy. This also checks absolute paths, since
|
||||||
|
// they'll resolve absolute and just ignore the project root.
|
||||||
|
$resolved_location = Filesystem::resolvePath(
|
||||||
|
$location,
|
||||||
|
$working_copy->getProjectRoot());
|
||||||
|
if (Filesystem::pathExists($resolved_location)) {
|
||||||
|
$location = $resolved_location;
|
||||||
|
$resolved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't find anything, check alongside the working copy.
|
||||||
|
if (!$resolved) {
|
||||||
|
$resolved_location = Filesystem::resolvePath(
|
||||||
|
$location,
|
||||||
|
dirname($working_copy->getProjectRoot()));
|
||||||
|
if (Filesystem::pathExists($resolved_location)) {
|
||||||
|
$location = $resolved_location;
|
||||||
|
$resolved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($config_trace_mode) {
|
||||||
|
echo "Loading phutil library from '{$location}'...\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = null;
|
||||||
|
try {
|
||||||
|
phutil_load_library($location);
|
||||||
|
} catch (PhutilBootloaderException $ex) {
|
||||||
|
$error = "Failed to load phutil library at location '{$location}'. ".
|
||||||
|
"This library is specified by {$lib_source}. Check that the ".
|
||||||
|
"setting is correct and the library is located in the right ".
|
||||||
|
"place.";
|
||||||
|
if ($must_load) {
|
||||||
|
throw new ArcanistUsageException($error);
|
||||||
|
} else {
|
||||||
|
file_put_contents(
|
||||||
|
'php://stderr',
|
||||||
|
phutil_console_wrap('WARNING: '.$error."\n\n"));
|
||||||
|
}
|
||||||
|
} catch (PhutilLibraryConflictException $ex) {
|
||||||
|
if ($ex->getLibrary() != 'arcanist') {
|
||||||
|
throw $ex;
|
||||||
|
}
|
||||||
|
$arc_dir = dirname(dirname(__FILE__));
|
||||||
|
$error =
|
||||||
|
"You are trying to run one copy of Arcanist on another copy of ".
|
||||||
|
"Arcanist. This operation is not supported. To execute Arcanist ".
|
||||||
|
"operations against this working copy, run './bin/arc' (from the ".
|
||||||
|
"current working copy) not some other copy of 'arc' (you ran one ".
|
||||||
|
"from '{$arc_dir}').";
|
||||||
|
throw new ArcanistUsageException($error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1153,4 +1153,16 @@ abstract class ArcanistBaseWorkflow {
|
||||||
return $this->repositoryEncoding;
|
return $this->repositoryEncoding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function formatConfigValueForDisplay($value) {
|
||||||
|
if (is_array($value)) {
|
||||||
|
// TODO: Both json_encode() and PhutilJSON do a bad job with one-liners.
|
||||||
|
// PhutilJSON splits them across a bunch of lines, while json_encode()
|
||||||
|
// escapes all kinds of stuff like "/". It would be nice if PhutilJSON
|
||||||
|
// had a mode for pretty one-liners.
|
||||||
|
$value = json_encode($value);
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,8 @@ EOTEXT
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($keys as $key) {
|
foreach ($keys as $key) {
|
||||||
echo "{$key} = ".idx($config, $key)."\n";
|
$val = self::formatConfigValueForDisplay(idx($config, $key));
|
||||||
|
echo "{$key} = {$val}\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
@ -139,7 +139,7 @@ EOTEXT
|
||||||
|
|
||||||
$engine = $this->getArgument('engine');
|
$engine = $this->getArgument('engine');
|
||||||
if (!$engine) {
|
if (!$engine) {
|
||||||
$engine = $working_copy->getConfig('lint_engine');
|
$engine = $working_copy->getConfigFromAnySource('lint.engine');
|
||||||
if (!$engine) {
|
if (!$engine) {
|
||||||
throw new ArcanistNoEngineException(
|
throw new ArcanistNoEngineException(
|
||||||
"No lint engine configured for this project. Edit .arcconfig to ".
|
"No lint engine configured for this project. Edit .arcconfig to ".
|
||||||
|
|
|
@ -37,20 +37,31 @@ EOTEXT
|
||||||
|
|
||||||
Values are written to '~/.arcrc' on Linux and Mac OS X, and an
|
Values are written to '~/.arcrc' on Linux and Mac OS X, and an
|
||||||
undisclosed location on Windows.
|
undisclosed location on Windows.
|
||||||
|
|
||||||
|
With __--show__, a description of supported configuration values
|
||||||
|
is shown.
|
||||||
EOTEXT
|
EOTEXT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getArguments() {
|
public function getArguments() {
|
||||||
return array(
|
return array(
|
||||||
|
'show' => array(
|
||||||
|
'help' => 'Show available configuration values.',
|
||||||
|
),
|
||||||
'*' => 'argv',
|
'*' => 'argv',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run() {
|
public function run() {
|
||||||
|
if ($this->getArgument('show')) {
|
||||||
|
return $this->show();
|
||||||
|
}
|
||||||
|
|
||||||
$argv = $this->getArgument('argv');
|
$argv = $this->getArgument('argv');
|
||||||
if (count($argv) != 2) {
|
if (count($argv) != 2) {
|
||||||
throw new ArcanistUsageException("Specify a key and a value.");
|
throw new ArcanistUsageException(
|
||||||
|
"Specify a key and a value, or --show.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$config = self::readGlobalArcConfig();
|
$config = self::readGlobalArcConfig();
|
||||||
|
@ -73,9 +84,14 @@ EOTEXT
|
||||||
echo "Deleted key '{$key}' (was '{$old}').\n";
|
echo "Deleted key '{$key}' (was '{$old}').\n";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
$val = $this->parse($key, $val);
|
||||||
|
|
||||||
$config[$key] = $val;
|
$config[$key] = $val;
|
||||||
self::writeGlobalArcConfig($config);
|
self::writeGlobalArcConfig($config);
|
||||||
|
|
||||||
|
$val = self::formatConfigValueForDisplay($val);
|
||||||
|
$old = self::formatConfigValueForDisplay($old);
|
||||||
|
|
||||||
if ($old === null) {
|
if ($old === null) {
|
||||||
echo "Set key '{$key}' = '{$val}'.\n";
|
echo "Set key '{$key}' = '{$val}'.\n";
|
||||||
} else {
|
} else {
|
||||||
|
@ -86,4 +102,83 @@ EOTEXT
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function show() {
|
||||||
|
$keys = array(
|
||||||
|
'default' => array(
|
||||||
|
'help' =>
|
||||||
|
'The URI of a Phabricator install to connect to by default, if '.
|
||||||
|
'arc is run in a project without a Phabricator URI or run outside '.
|
||||||
|
'of a project.',
|
||||||
|
'example' => 'http://phabricator.example.com/',
|
||||||
|
),
|
||||||
|
'load' => array(
|
||||||
|
'help' =>
|
||||||
|
'A list of paths to phutil libraries that should be loaded at '.
|
||||||
|
'startup. This can be used to make classes available, like lint or '.
|
||||||
|
'unit test engines.',
|
||||||
|
'example' => '["/var/arc/customlib/src"]',
|
||||||
|
),
|
||||||
|
'lint.engine' => array(
|
||||||
|
'help' =>
|
||||||
|
'The name of a default lint engine to use, if no lint engine is '.
|
||||||
|
'specified by the current project.',
|
||||||
|
'example' => 'ExampleLintEngine',
|
||||||
|
),
|
||||||
|
'unit.engine' => array(
|
||||||
|
'help' =>
|
||||||
|
'The name of a default unit test engine to use, if no unit test '.
|
||||||
|
'engine is specified by the current project.',
|
||||||
|
'example' => 'ExampleUnitTestEngine',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$config = self::readGlobalArcConfig();
|
||||||
|
|
||||||
|
foreach ($keys as $key => $spec) {
|
||||||
|
$type = $this->getType($key);
|
||||||
|
|
||||||
|
$value = idx($config, $key);
|
||||||
|
$value = self::formatConfigValueForDisplay($value);
|
||||||
|
|
||||||
|
echo phutil_console_format("**__%s__** (%s)\n\n", $key, $type);
|
||||||
|
echo phutil_console_format(" Example: %s\n", $spec['example']);
|
||||||
|
if (strlen($value)) {
|
||||||
|
echo phutil_console_format(" Global Setting: %s\n", $value);
|
||||||
|
}
|
||||||
|
echo "\n";
|
||||||
|
echo phutil_console_wrap($spec['help'], 4);
|
||||||
|
echo "\n\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getType($key) {
|
||||||
|
static $types = array(
|
||||||
|
'load' => 'list',
|
||||||
|
);
|
||||||
|
|
||||||
|
return idx($types, $key, 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parse($key, $val) {
|
||||||
|
$type = $this->getType($key);
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case 'string':
|
||||||
|
return $val;
|
||||||
|
case 'list':
|
||||||
|
$val = json_decode($val, true);
|
||||||
|
if (!is_array($val)) {
|
||||||
|
$example = '["apple", "banana", "cherry"]';
|
||||||
|
throw new ArcanistUsageException(
|
||||||
|
"Value for key '{$key}' must be specified as a JSON-encoded ".
|
||||||
|
"list. Example: {$example}");
|
||||||
|
}
|
||||||
|
return $val;
|
||||||
|
default:
|
||||||
|
throw new Exception("Unknown config key type '{$type}'!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,7 @@ EOTEXT
|
||||||
|
|
||||||
$engine_class = $this->getArgument(
|
$engine_class = $this->getArgument(
|
||||||
'engine',
|
'engine',
|
||||||
$working_copy->getConfig('unit_engine'));
|
$working_copy->getConfigFromAnySource('unit.engine'));
|
||||||
|
|
||||||
if (!$engine_class) {
|
if (!$engine_class) {
|
||||||
throw new ArcanistNoEngineException(
|
throw new ArcanistNoEngineException(
|
||||||
|
|
|
@ -19,6 +19,9 @@
|
||||||
/**
|
/**
|
||||||
* Interfaces with basic information about the working copy.
|
* Interfaces with basic information about the working copy.
|
||||||
*
|
*
|
||||||
|
*
|
||||||
|
* @task config
|
||||||
|
*
|
||||||
* @group workingcopy
|
* @group workingcopy
|
||||||
*/
|
*/
|
||||||
final class ArcanistWorkingCopyIdentity {
|
final class ArcanistWorkingCopyIdentity {
|
||||||
|
@ -108,11 +111,59 @@ final class ArcanistWorkingCopyIdentity {
|
||||||
return $this->getConfig('conduit_uri');
|
return $this->getConfig('conduit_uri');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getConfig($key) {
|
|
||||||
if (!empty($this->projectConfig[$key])) {
|
/* -( Config )------------------------------------------------------------- */
|
||||||
return $this->projectConfig[$key];
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a configuration directive from project configuration. This reads ONLY
|
||||||
|
* permanent project configuration (i.e., ".arcconfig"), not other
|
||||||
|
* configuration sources. See @{method:getConfigFromAnySource} to read from
|
||||||
|
* user configuration.
|
||||||
|
*
|
||||||
|
* @param key Key to read.
|
||||||
|
* @param wild Default value if key is not found.
|
||||||
|
* @return wild Value, or default value if not found.
|
||||||
|
*
|
||||||
|
* @task config
|
||||||
|
*/
|
||||||
|
public function getConfig($key, $default = null) {
|
||||||
|
return idx($this->projectConfig, $key, $default);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a configuration directive from any available configuration source.
|
||||||
|
* In contrast to @{method:getConfig}, this will look for the directive in
|
||||||
|
* user configuration in addition to project configuration.
|
||||||
|
*
|
||||||
|
* @param key Key to read.
|
||||||
|
* @param wild Default value if key is not found.
|
||||||
|
* @return wild Value, or default value if not found.
|
||||||
|
*
|
||||||
|
* @task config
|
||||||
|
*/
|
||||||
|
public function getConfigFromAnySource($key, $default = null) {
|
||||||
|
$pval = $this->getConfig($key);
|
||||||
|
if ($pval !== null) {
|
||||||
|
return $pval;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for older names.
|
||||||
|
|
||||||
|
static $deprecated_names = array(
|
||||||
|
'lint.engine' => 'lint_engine',
|
||||||
|
'unit.engine' => 'unit_engine',
|
||||||
|
);
|
||||||
|
if (isset($deprecated_names[$key])) {
|
||||||
|
$pval = $this->getConfig($deprecated_names[$key]);
|
||||||
|
if ($pval !== null) {
|
||||||
|
return $pval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$global_config = ArcanistBaseWorkflow::readGlobalArcConfig();
|
||||||
|
return idx($global_config, $key, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue