diff --git a/bin/arc b/bin/arc index e125698b..2c7b2cc3 100755 --- a/bin/arc +++ b/bin/arc @@ -1,21 +1,14 @@ -#!/usr/bin/env bash +#!/usr/bin/env php +execute($argv); -exec "$DIR/../scripts/arcanist.php" "$@" diff --git a/bin/arc.bat b/bin/arc.bat index 5ce474bf..21735435 100644 --- a/bin/arc.bat +++ b/bin/arc.bat @@ -1,2 +1,2 @@ @echo off -php -f "%~dp0..\scripts\arcanist.php" -- %* +php -f "%~dp0..\bin\arc" -- %* diff --git a/bin/phage b/bin/phage deleted file mode 120000 index 0817a287..00000000 --- a/bin/phage +++ /dev/null @@ -1 +0,0 @@ -../scripts/phage.php \ No newline at end of file diff --git a/bin/phage b/bin/phage new file mode 100755 index 00000000..2c7b2cc3 --- /dev/null +++ b/bin/phage @@ -0,0 +1,14 @@ +#!/usr/bin/env php +execute($argv); + diff --git a/scripts/arcanist.php b/scripts/arcanist.php index 6743f394..d9ce34a5 100755 --- a/scripts/arcanist.php +++ b/scripts/arcanist.php @@ -1,699 +1,299 @@ #!/usr/bin/env php parseStandardArguments(); -$base_args->parsePartial( - array( - array( - 'name' => 'load-phutil-library', - 'param' => 'path', - 'help' => pht('Load a libphutil library.'), - 'repeat' => true, - ), - array( - 'name' => 'skip-arcconfig', - ), - array( - 'name' => 'arcrc-file', - 'param' => 'filename', - ), - array( - 'name' => 'conduit-uri', - 'param' => 'uri', - 'help' => pht('Connect to Phabricator install specified by __uri__.'), - ), - array( - 'name' => 'conduit-token', - 'param' => 'token', - 'help' => pht('Use a specific authentication token.'), - ), - array( - 'name' => 'anonymous', - 'help' => pht('Run workflow as a public user, without authenticating.'), - ), - array( - 'name' => 'conduit-version', - 'param' => 'version', - 'help' => pht( - '(Developers) Mock client version in protocol handshake.'), - ), - array( - 'name' => 'conduit-timeout', - 'param' => 'timeout', - 'help' => pht('Set Conduit timeout (in seconds).'), - ), - array( - 'name' => 'config', - 'param' => 'key=value', - 'repeat' => true, - 'help' => pht( - 'Specify a runtime configuration value. This will take precedence '. - 'over static values, and only affect the current arcanist invocation.'), - ), -)); +if (function_exists('pcntl_async_signals')) { + pcntl_async_signals(true); +} else { + declare(ticks = 1); +} + +require_once dirname(dirname(__FILE__)).'/scripts/init/init-arcanist.php'; + +$runtime = new ArcanistRuntime(); +return $runtime->execute($argv); + $config_trace_mode = $base_args->getArg('trace'); $force_conduit = $base_args->getArg('conduit-uri'); $force_token = $base_args->getArg('conduit-token'); -$force_conduit_version = $base_args->getArg('conduit-version'); -$conduit_timeout = $base_args->getArg('conduit-timeout'); -$skip_arcconfig = $base_args->getArg('skip-arcconfig'); -$custom_arcrc = $base_args->getArg('arcrc-file'); $is_anonymous = $base_args->getArg('anonymous'); $load = $base_args->getArg('load-phutil-library'); $help = $base_args->getArg('help'); $args = array_values($base_args->getUnconsumedArgumentVector()); -$working_directory = getcwd(); $console = PhutilConsole::getConsole(); $config = null; $workflow = null; try { - if ($config_trace_mode) { - echo tsprintf( - "** %s ** %s\n", - pht('ARGV'), - csprintf('%Ls', $original_argv)); - - $libraries = array( - 'phutil', - 'arcanist', - ); - - foreach ($libraries as $library_name) { - echo tsprintf( - "** %s ** %s\n", - pht('LOAD'), - pht( - 'Loaded "%s" from "%s".', - $library_name, - phutil_get_library_root($library_name))); - } - } - - if (!$args) { - if ($help) { - $args = array('help'); - } else { - throw new ArcanistUsageException( - pht('No command provided. Try `%s`.', 'arc help')); - } - } else if ($help) { - array_unshift($args, 'help'); - } - - $configuration_manager = new ArcanistConfigurationManager(); - if ($custom_arcrc) { - $configuration_manager->setUserConfigurationFileLocation($custom_arcrc); - } - - $global_config = $configuration_manager->readUserArcConfig(); - $system_config = $configuration_manager->readSystemArcConfig(); - $runtime_config = $configuration_manager->applyRuntimeArcConfig($base_args); - - if ($skip_arcconfig) { - $working_copy = ArcanistWorkingCopyIdentity::newDummyWorkingCopy(); - } else { - $working_copy = - ArcanistWorkingCopyIdentity::newFromPath($working_directory); - } - $configuration_manager->setWorkingCopyIdentity($working_copy); - - // 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) { - $console->writeLog( - "%s\n", - pht( - 'Using `%s` flag, configuration will be ignored and configured '. - 'libraries will not be loaded.', - '--load-phutil-library')); - // Load the flag libraries. These must load, since the user specified them - // explicitly. - arcanist_load_libraries( - $load, - $must_load = true, - $lib_source = pht('a "%s" flag', '--load-phutil-library'), - $working_copy); - } else { - // Load libraries in system 'load' config. In contrast to global config, we - // fail hard here because this file is edited manually, so if 'arc' breaks - // that doesn't make it any more difficult to correct. - arcanist_load_libraries( - idx($system_config, 'load', array()), - $must_load = true, - $lib_source = pht('the "%s" setting in system config', 'load'), - $working_copy); - - // 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 - // from running "arc set-config" to correct them. - arcanist_load_libraries( - idx($global_config, 'load', array()), - $must_load = false, - $lib_source = pht('the "%s" setting in global config', 'load'), - $working_copy); - - // Load libraries in ".arcconfig". Libraries here must load. - arcanist_load_libraries( - $working_copy->getProjectConfig('load'), - $must_load = true, - $lib_source = pht('the "%s" setting in "%s"', 'load', '.arcconfig'), - $working_copy); - - // Load libraries in ".arcconfig". Libraries here must load. - arcanist_load_libraries( - idx($runtime_config, 'load', array()), - $must_load = true, - $lib_source = pht('the %s argument', '--config "load=[...]"'), - $working_copy); - } - - $user_config = $configuration_manager->readUserConfigurationFile(); - - $config_class = $working_copy->getProjectConfig('arcanist_configuration'); - if ($config_class) { - $config = new $config_class(); - } else { - $config = new ArcanistConfiguration(); - } $command = strtolower($args[0]); $args = array_slice($args, 1); $workflow = $config->selectWorkflow( $command, $args, $configuration_manager, $console); $workflow->setConfigurationManager($configuration_manager); $workflow->setArcanistConfiguration($config); $workflow->setCommand($command); $workflow->setWorkingDirectory($working_directory); $workflow->parseArguments($args); // Write the command into the environment so that scripts (for example, local // Git commit hooks) can detect that they're being run via `arc` and change // their behaviors. putenv('ARCANIST='.$command); - if ($force_conduit_version) { - $workflow->forceConduitVersion($force_conduit_version); - } - if ($conduit_timeout) { - $workflow->setConduitTimeout($conduit_timeout); - } - $need_working_copy = $workflow->requiresWorkingCopy(); $supported_vcs_types = $workflow->getSupportedRevisionControlSystems(); $vcs_type = $working_copy->getVCSType(); if ($vcs_type || $need_working_copy) { if (!in_array($vcs_type, $supported_vcs_types)) { throw new ArcanistUsageException( pht( '`%s %s` is only supported under %s.', 'arc', $workflow->getWorkflowName(), implode(', ', $supported_vcs_types))); } } $need_conduit = $workflow->requiresConduit(); $need_auth = $workflow->requiresAuthentication(); $need_repository_api = $workflow->requiresRepositoryAPI(); $want_repository_api = $workflow->desiresRepositoryAPI(); $want_working_copy = $workflow->desiresWorkingCopy() || $want_repository_api; $need_conduit = $need_conduit || $need_auth; $need_working_copy = $need_working_copy || $need_repository_api; if ($need_working_copy || $want_working_copy) { if ($need_working_copy && !$working_copy->getVCSType()) { throw new ArcanistUsageException( pht( 'This command must be run in a Git, Mercurial or Subversion '. 'working copy.')); } $configuration_manager->setWorkingCopyIdentity($working_copy); } if ($force_conduit) { $conduit_uri = $force_conduit; } else { $conduit_uri = $configuration_manager->getConfigFromAnySource( 'phabricator.uri'); if ($conduit_uri === null) { $conduit_uri = $configuration_manager->getConfigFromAnySource('default'); } } if ($conduit_uri) { // Set the URI path to '/api/'. TODO: Originally, I contemplated letting // you deploy Phabricator somewhere other than the domain root, but ended // up never pursuing that. We should get rid of all "/api/" silliness // in things users are expected to configure. This is already happening // to some degree, e.g. "arc install-certificate" does it for you. $conduit_uri = new PhutilURI($conduit_uri); $conduit_uri->setPath('/api/'); $conduit_uri = (string)$conduit_uri; } $workflow->setConduitURI($conduit_uri); // Apply global CA bundle from configs. $ca_bundle = $configuration_manager->getConfigFromAnySource('https.cabundle'); if ($ca_bundle) { $ca_bundle = Filesystem::resolvePath( $ca_bundle, $working_copy->getProjectRoot()); HTTPSFuture::setGlobalCABundleFromPath($ca_bundle); } $blind_key = 'https.blindly-trust-domains'; $blind_trust = $configuration_manager->getConfigFromAnySource($blind_key); if ($blind_trust) { $trust_extension = PhutilHTTPEngineExtension::requireExtension( ArcanistBlindlyTrustHTTPEngineExtension::EXTENSIONKEY); $trust_extension->setDomains($blind_trust); } if ($need_conduit) { if (!$conduit_uri) { $message = phutil_console_format( "%s\n\n - %s\n - %s\n - %s\n", pht( 'This command requires arc to connect to a Phabricator install, '. 'but no Phabricator installation is configured. To configure a '. 'Phabricator URI:'), pht( 'set a default location with `%s`; or', 'arc set-config default '), pht( 'specify `%s` explicitly; or', '--conduit-uri=uri'), pht( "run `%s` in a working copy with an '%s'.", 'arc', '.arcconfig')); $message = phutil_console_wrap($message); throw new ArcanistUsageException($message); } $workflow->establishConduit(); } $hosts_config = idx($user_config, 'hosts', array()); $host_config = idx($hosts_config, $conduit_uri, array()); $user_name = idx($host_config, 'user'); $certificate = idx($host_config, 'cert'); $conduit_token = idx($host_config, 'token'); if ($force_token) { $conduit_token = $force_token; } if ($is_anonymous) { $conduit_token = null; } $description = implode(' ', $original_argv); $credentials = array( 'user' => $user_name, 'certificate' => $certificate, 'description' => $description, 'token' => $conduit_token, ); $workflow->setConduitCredentials($credentials); $basic_user = $configuration_manager->getConfigFromAnySource( 'http.basicauth.user'); $basic_pass = $configuration_manager->getConfigFromAnySource( 'http.basicauth.pass'); $engine = id(new ArcanistConduitEngine()) ->setConduitURI($conduit_uri) ->setConduitToken($conduit_token) ->setBasicAuthUser($basic_user) ->setBasicAuthPass($basic_pass); - if ($conduit_timeout) { - $engine->setConduitTimeout($conduit_timeout); - } - $workflow->setConduitEngine($engine); if ($need_auth) { if ((!$user_name || !$certificate) && (!$conduit_token)) { $arc = 'arc'; if ($force_conduit) { $arc .= csprintf(' --conduit-uri=%s', $conduit_uri); } $conduit_domain = id(new PhutilURI($conduit_uri))->getDomain(); throw new ArcanistUsageException( phutil_console_format( "%s\n\n%s\n\n%s **%s:**\n\n $ **{$arc} install-certificate**\n", pht('YOU NEED TO AUTHENTICATE TO CONTINUE'), pht( 'You are trying to connect to a server (%s) that you '. 'do not have any credentials stored for.', $conduit_domain), pht('To retrieve and store credentials for this server,'), pht('run this command'))); } $workflow->authenticateConduit(); } if ($need_repository_api || ($want_repository_api && $working_copy->getVCSType())) { $repository_api = ArcanistRepositoryAPI::newAPIFromConfigurationManager( $configuration_manager); $workflow->setRepositoryAPI($repository_api); } $listeners = $configuration_manager->getConfigFromAnySource( 'events.listeners'); if ($listeners) { foreach ($listeners as $listener) { $console->writeLog( "%s\n", pht("Registering event listener '%s'.", $listener)); try { id(new $listener())->register(); } catch (PhutilMissingSymbolException $ex) { // Continue anyway, since you may otherwise be unable to run commands // like `arc set-config events.listeners` in order to repair the damage // you've caused. We're writing out the entire exception here because // it might not have been triggered by the listener itself (for example, // the listener might use a bad class in its register() method). $console->writeErr( "%s\n", pht( "ERROR: Failed to load event listener '%s': %s", $listener, $ex->getMessage())); } } } $config->willRunWorkflow($command, $workflow); $workflow->willRunWorkflow(); try { $err = $workflow->run(); $config->didRunWorkflow($command, $workflow, $err); } catch (Exception $e) { $workflow->finalize(); throw $e; } $workflow->finalize(); exit((int)$err); } catch (ArcanistNoEffectException $ex) { echo $ex->getMessage()."\n"; } catch (Exception $ex) { $is_usage = ($ex instanceof ArcanistUsageException); if ($is_usage) { fwrite(STDERR, phutil_console_format( "**%s** %s\n", pht('Usage Exception:'), rtrim($ex->getMessage()))); } if ($config) { $config->didAbortWorkflow($command, $workflow, $ex); } if ($config_trace_mode) { fwrite(STDERR, "\n"); throw $ex; } if (!$is_usage) { fwrite(STDERR, phutil_console_format( "** %s **\n", pht('Exception'))); while ($ex) { fwrite(STDERR, $ex->getMessage()."\n"); if ($ex instanceof PhutilProxyException) { $ex = $ex->getPreviousException(); } else { $ex = null; } } fwrite(STDERR, phutil_console_format( "(%s)\n", pht('Run with `%s` for a full exception trace.', '--trace'))); } exit(1); } -/** - * Perform some sanity checks against the possible diversity of PHP builds in - * the wild, like very old versions and builds that were compiled with flags - * that exclude core functionality. - */ -function sanity_check_environment() { - // NOTE: We don't have phutil_is_windows() yet here. - $is_windows = (DIRECTORY_SEPARATOR != '/'); - - // We use stream_socket_pair() which is not available on Windows earlier. - $min_version = ($is_windows ? '5.3.0' : '5.2.3'); - $cur_version = phpversion(); - if (version_compare($cur_version, $min_version, '<')) { - die_with_bad_php( - "You are running PHP version '{$cur_version}', which is older than ". - "the minimum version, '{$min_version}'. Update to at least ". - "'{$min_version}'."); - } - - if ($is_windows) { - $need_functions = array( - 'curl_init' => array('builtin-dll', 'php_curl.dll'), - ); - } else { - $need_functions = array( - 'curl_init' => array( - 'text', - "You need to install the cURL PHP extension, maybe with ". - "'apt-get install php5-curl' or 'yum install php53-curl' or ". - "something similar.", - ), - 'json_decode' => array('flag', '--without-json'), - ); - } - - $problems = array(); - - $config = null; - $show_config = false; - foreach ($need_functions as $fname => $resolution) { - if (function_exists($fname)) { - continue; - } - - static $info; - if ($info === null) { - ob_start(); - phpinfo(INFO_GENERAL); - $info = ob_get_clean(); - $matches = null; - if (preg_match('/^Configure Command =>\s*(.*?)$/m', $info, $matches)) { - $config = $matches[1]; - } - } - - $generic = true; - list($what, $which) = $resolution; - - if ($what == 'flag' && strpos($config, $which) !== false) { - $show_config = true; - $generic = false; - $problems[] = - "This build of PHP was compiled with the configure flag '{$which}', ". - "which means it does not have the function '{$fname}()'. This ". - "function is required for arc to run. Rebuild PHP without this flag. ". - "You may also be able to build or install the relevant extension ". - "separately."; - } - - if ($what == 'builtin-dll') { - $generic = false; - $problems[] = - "Your install of PHP does not have the '{$which}' extension enabled. ". - "Edit your php.ini file and uncomment the line which reads ". - "'extension={$which}'."; - } - - if ($what == 'text') { - $generic = false; - $problems[] = $which; - } - - if ($generic) { - $problems[] = - "This build of PHP is missing the required function '{$fname}()'. ". - "Rebuild PHP or install the extension which provides '{$fname}()'."; - } - } - - if ($problems) { - if ($show_config) { - $problems[] = "PHP was built with this configure command:\n\n{$config}"; - } - die_with_bad_php(implode("\n\n", $problems)); - } -} - -function die_with_bad_php($message) { - // NOTE: We're bailing because PHP is broken. We can't call any library - // functions because they won't be loaded yet. - - echo "\n"; - echo 'PHP CONFIGURATION ERRORS'; - echo "\n\n"; - echo $message; - echo "\n\n"; - exit(1); -} - -function arcanist_load_libraries( - $load, - $must_load, - $lib_source, - ArcanistWorkingCopyIdentity $working_copy) { - - if (!$load) { - return; - } - if (!is_array($load)) { - $error = pht( - 'Libraries specified by %s are invalid; expected a list. '. - 'Check your configuration.', - $lib_source); - $console = PhutilConsole::getConsole(); - $console->writeErr("%s: %s\n", pht('WARNING'), $error); - 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; - } - } - $console = PhutilConsole::getConsole(); - $console->writeLog( - "%s\n", - pht("Loading phutil library from '%s'...", $location)); - - $error = null; - try { - phutil_load_library($location); - } catch (PhutilBootloaderException $ex) { - $error = pht( - "Failed to load phutil library at location '%s'. This library ". - "is specified by %s. Check that the setting is correct and the ". - "library is located in the right place.", - $location, - $lib_source); - if ($must_load) { - throw new ArcanistUsageException($error); - } else { - fwrite(STDERR, phutil_console_wrap( - phutil_console_format("%s: %s\n", - pht('WARNING'), - $error))); - } - } catch (PhutilLibraryConflictException $ex) { - if ($ex->getLibrary() != 'arcanist') { - throw $ex; - } - - // NOTE: If you are running `arc` against itself, we ignore the library - // conflict created by loading the local `arc` library (in the current - // working directory) and continue without loading it. - - // This means we only execute code in the `arcanist/` directory which is - // associated with the binary you are running, whereas we would normally - // execute local code. - - // This can make `arc` development slightly confusing if your setup is - // especially bizarre, but it allows `arc` to be used in automation - // workflows more easily. For some context, see PHI13. - - $executing_directory = dirname(dirname(__FILE__)); - $working_directory = dirname($location); - - fwrite( - STDERR, - tsprintf( - "** %s ** %s\n", - pht('VERY META'), - pht( - 'You are running one copy of Arcanist (at path "%s") against '. - 'another copy of Arcanist (at path "%s"). Code in the current '. - 'working directory will not be loaded or executed.', - $executing_directory, - $working_directory))); - } - } -} diff --git a/scripts/init/init-arcanist.php b/scripts/init/init-arcanist.php new file mode 100644 index 00000000..2dc4fb65 --- /dev/null +++ b/scripts/init/init-arcanist.php @@ -0,0 +1,578 @@ +checkEnvironment(); + if ($err) { + return $err; + } + + $err = $this->includeCoreLibraries(); + if ($err) { + return $err; + } + + PhutilTranslator::getInstance() + ->setLocale(PhutilLocale::loadLocale('en_US')) + ->setTranslations(PhutilTranslation::getTranslationMapForLocale('en_US')); + + ini_set('memory_limit', -1); + + try { + return $this->executeCore($argv); + } catch (PhutilArgumentUsageException $ex) { + fwrite( + STDERR, + tsprintf( + "**%s:** %s\n", + pht('Usage Exception'), + $ex->getMessage())); + return 77; + } + } + + private function executeCore(array $argv) { + $config_args = array( + array( + 'name' => 'load-phutil-library', + 'param' => 'path', + 'help' => pht('Load a libphutil library.'), + 'repeat' => true, + ), + array( + 'name' => 'config', + 'param' => 'key=value', + 'repeat' => true, + 'help' => pht('Specify a runtime configuration value.'), + ), + ); + + $args = id(new PhutilArgumentParser($argv)) + ->parseStandardArguments(); + + $is_trace = $args->getArg('trace'); + if ($is_trace) { + $this->logTrace(pht('ARGV'), csprintf('%Ls', $argv)); + } + + $args->parsePartial($config_args, true); + + $config = $this->loadConfiguration($args); + + $this->loadLibraries($args, $config); + + $toolset = $this->newToolset($argv); + + $args->parsePartial($toolset->getToolsetArguments()); + + $workflows = $this->newWorkflows($toolset); + + $phutil_workflows = array(); + foreach ($workflows as $key => $workflow) { + $phutil_workflows[$key] = $workflow->newPhutilWorkflow(); + } + + $unconsumed_argv = $args->getUnconsumedArgumentVector(); + + $result = $this->resolveAliases($workflows, $unconsumed_argv, $config); + if (is_int($result)) { + return $result; + } + + $args->setUnconsumedArgumentVector($result); + + return $args->parseWorkflows($phutil_workflows); + } + + + /** + * Perform some sanity checks against the possible diversity of PHP builds in + * the wild, like very old versions and builds that were compiled with flags + * that exclude core functionality. + */ + private function checkEnvironment() { + // NOTE: We don't have phutil_is_windows() yet here. + $is_windows = (DIRECTORY_SEPARATOR != '/'); + + // We use stream_socket_pair() which is not available on Windows earlier. + $min_version = ($is_windows ? '5.3.0' : '5.2.3'); + $cur_version = phpversion(); + if (version_compare($cur_version, $min_version, '<')) { + $message = sprintf( + 'You are running a version of PHP ("%s"), which is older than the '. + 'minimum supported version ("%s"). Update PHP to continue.', + $cur_version, + $min_version); + return $this->fatalError($message); + } + + if ($is_windows) { + $need_functions = array( + 'curl_init' => array('builtin-dll', 'php_curl.dll'), + ); + } else { + $need_functions = array( + 'curl_init' => array( + 'text', + "You need to install the cURL PHP extension, maybe with ". + "'apt-get install php5-curl' or 'yum install php53-curl' or ". + "something similar.", + ), + 'json_decode' => array('flag', '--without-json'), + ); + } + + $problems = array(); + + $config = null; + $show_config = false; + foreach ($need_functions as $fname => $resolution) { + if (function_exists($fname)) { + continue; + } + + static $info; + if ($info === null) { + ob_start(); + phpinfo(INFO_GENERAL); + $info = ob_get_clean(); + $matches = null; + if (preg_match('/^Configure Command =>\s*(.*?)$/m', $info, $matches)) { + $config = $matches[1]; + } + } + + list($what, $which) = $resolution; + + if ($what == 'flag' && strpos($config, $which) !== false) { + $show_config = true; + $problems[] = sprintf( + 'The build of PHP you are running was compiled with the configure '. + 'flag "%s", which means it does not support the function "%s()". '. + 'This function is required for Arcanist to run. Install a standard '. + 'build of PHP or rebuild it without this flag. You may also be '. + 'able to build or install the relevant extension separately.', + $which, + $fname); + continue; + } + + if ($what == 'builtin-dll') { + $problems[] = sprintf( + 'The build of PHP you are running does not have the "%s" extension '. + 'enabled. Edit your php.ini file and uncomment the line which '. + 'reads "extension=%s".', + $which, + $which); + continue; + } + + if ($what == 'text') { + $problems[] = $which; + continue; + } + + $problems[] = sprintf( + 'The build of PHP you are running is missing the required function '. + '"%s()". Rebuild PHP or install the extension which provides "%s()".', + $fname, + $fname); + } + + if ($problems) { + if ($show_config) { + $problems[] = "PHP was built with this configure command:\n\n{$config}"; + } + $problems = implode("\n\n", $problems); + return $this->fatalError($problems); + } + + return 0; + } + + private function fatalError($message) { + echo "CONFIGURATION ERROR\n\n"; + echo $message; + echo "\n\n"; + return 1; + } + + private function includeCoreLibraries() { + // Adjust 'include_path' to add locations where we'll search for libphutil. + // We look in these places: + // + // - Next to 'arcanist/'. + // - Anywhere in the normal PHP 'include_path'. + // - Inside 'arcanist/externals/includes/'. + // + // When looking in these places, we expect to find a 'libphutil/' directory. + + // The 'arcanist/' directory. + $arcanist_dir = dirname(dirname(dirname(__FILE__))); + + // The parent directory of 'arcanist/'. + $parent_dir = dirname($arcanist_dir); + + // The 'arcanist/externals/includes/' directory. + $include_dir = implode( + DIRECTORY_SEPARATOR, + array( + $arcanist_dir, + 'externals', + 'includes', + )); + + $php_include_path = ini_get('include_path'); + $php_include_path = implode( + PATH_SEPARATOR, + array( + $parent_dir, + $php_include_path, + $include_dir, + )); + + ini_set('include_path', $php_include_path); + + // Load libphutil. + @include_once 'libphutil/scripts/__init_script__.php'; + + if (!@constant('__LIBPHUTIL__')) { + return $this->fatalError( + 'Unable to load libphutil. Put "libphutil/" next to "arcanist/"; '. + 'or update your PHP "include_path" to include the parent directory '. + 'of "libphutil/"; or symlink "libphutil" into '. + '"arcanist/externals/includes/".'); + } + + // Load Arcanist. + phutil_load_library($arcanist_dir.'/src/'); + + return 0; + } + + private function loadConfiguration(PhutilArgumentParser $args) { + $configuration_manager = new ArcanistConfigurationManager(); + + $cwd = getcwd(); + $working_copy = ArcanistWorkingCopyIdentity::newFromPath($cwd); + $configuration_manager->setWorkingCopyIdentity($working_copy); + + $configuration_manager->applyRuntimeArcConfig($args); + + return $configuration_manager; + } + + private function loadLibraries( + PhutilArgumentParser $args, + ArcanistConfigurationManager $config) { + + $is_trace = $args->getArg('trace'); + + if ($is_trace) { + $libraries = array( + 'phutil', + 'arcanist', + ); + + foreach ($libraries as $library_name) { + $this->logTrace( + pht('LOAD'), + pht( + 'Loaded "%s" from "%s".', + $library_name, + phutil_get_library_root($library_name))); + } + } + + $load = array(); + $working_copy = $config->getWorkingCopyIdentity(); + + $cli_libraries = $args->getArg('load-phutil-library'); + if ($cli_libraries) { + $load[] = array( + '--load-phutil-library', + $cli_libraries, + ); + } else { + $system_config = $config->readSystemArcConfig(); + $load[] = array( + $config->getSystemArcConfigLocation(), + idx($system_config, 'load', array()), + ); + + $global_config = $config->readUserArcConfig(); + $load[] = array( + $config->getUserConfigurationFileLocation(), + idx($global_config, 'load', array()), + ); + + $load[] = array( + '.arcconfig', + $working_copy->getProjectConfig('load'), + ); + + $load[] = array( + // TODO: We could explain exactly where this is coming from more + // clearly. + './.../arc/config', + $working_copy->getLocalConfig('load'), + ); + + $load[] = array( + '--config load=...', + $config->getRuntimeConfig('load', array()), + ); + } + + foreach ($load as $spec) { + list($source, $libraries) = $spec; + if ($is_trace) { + $this->logTrace( + pht('LOAD'), + pht( + 'Loading libraries from "%s"...', + $source)); + } + + if (!$libraries) { + if ($is_trace) { + $this->logTrace(pht('NONE'), pht('Nothing to load.')); + } + continue; + } + + if (!is_array($libraries)) { + throw new PhutilArgumentUsageException( + pht( + 'Libraries specified by "%s" are not formatted correctly. '. + 'Expected a list of paths. Check your configuration.', + $source)); + } + + foreach ($libraries as $library) { + $this->loadLibrary($source, $library, $working_copy, $is_trace); + } + } + } + + private function loadLibrary( + $source, + $location, + ArcanistWorkingCopyIdentity $working_copy, + $is_trace) { + + // 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 ($is_trace) { + $this->logTrace( + pht('LOAD'), + pht('Loading phutil library from "%s"...', $location)); + } + + $error = null; + try { + phutil_load_library($location); + } catch (PhutilBootloaderException $ex) { + fwrite( + STDERR, + "%s", + tsprintf( + "** %s ** %s\n", + pht( + 'Failed to load phutil library at location "%s". This library '. + 'is specified by "%s". Check that the setting is correct and '. + 'the library is located in the right place.', + $location, + $source))); + + $prompt = pht('Continue without loading library?'); + if (!phutil_console_confirm($prompt)) { + throw $ex; + } + } catch (PhutilLibraryConflictException $ex) { + if ($ex->getLibrary() != 'arcanist') { + throw $ex; + } + + // NOTE: If you are running `arc` against itself, we ignore the library + // conflict created by loading the local `arc` library (in the current + // working directory) and continue without loading it. + + // This means we only execute code in the `arcanist/` directory which is + // associated with the binary you are running, whereas we would normally + // execute local code. + + // This can make `arc` development slightly confusing if your setup is + // especially bizarre, but it allows `arc` to be used in automation + // workflows more easily. For some context, see PHI13. + + $executing_directory = dirname(dirname(__FILE__)); + $working_directory = dirname($location); + + fwrite( + STDERR, + tsprintf( + "** %s ** %s\n", + pht('VERY META'), + pht( + 'You are running one copy of Arcanist (at path "%s") against '. + 'another copy of Arcanist (at path "%s"). Code in the current '. + 'working directory will not be loaded or executed.', + $executing_directory, + $working_directory))); + } + } + + private function newToolset(array $argv) { + $binary = basename($argv[0]); + + $toolsets = ArcanistToolset::newToolsetMap(); + if (!isset($toolsets[$binary])) { + throw new PhutilArgumentUsageException( + pht( + 'Arcanist toolset "%s" is unknown. The Arcanist binary should '. + 'be executed so that "argv[0]" identifies a supported toolset. '. + 'Rename the binary or install the library that provides the '. + 'desired toolset. Current available toolsets: %s.', + $binary, + implode(', ', array_keys($toolsets)))); + } + + return $toolsets[$binary]; + } + + private function newWorkflows(ArcanistToolset $toolset) { + $workflows = id(new PhutilClassMapQuery()) + ->setAncestorClass('ArcanistWorkflow') + ->execute(); + + foreach ($workflows as $key => $workflow) { + if (!$workflow->supportsToolset($toolset)) { + unset($workflows[$key]); + } + } + + $map = array(); + foreach ($workflows as $workflow) { + $key = $workflow->getWorkflowName(); + if (isset($map[$key])) { + throw new Exception( + pht( + 'Two workflows ("%s" and "%s") both have the same name ("%s") '. + 'and both support the current toolset ("%s", "%s"). Each '. + 'workflow in a given toolset must have a unique name.', + get_class($workflow), + get_class($map[$key]), + get_class($toolset), + $toolset->getToolsetKey())); + } + $map[$key] = id(clone $workflow) + ->setToolset($toolset); + } + + return $map; + } + + private function resolveAliases( + array $workflows, + array $argv, + ArcanistConfigurationManager $config) { + + $command = head($argv); + + // If this is a match for a recognized workflow, just return the arguments + // unmodified. You aren't allowed to alias over real workflows. + if (isset($workflows[$command])) { + return $argv; + } + + $aliases = ArcanistAliasWorkflow::getAliases($config); + list($new_command, $new_args) = ArcanistAliasWorkflow::resolveAliases( + $command, + $this, + array_slice($argv, 1), + $config); + + // You can't alias something to itself, so if the new command isn't new, + // we're all done resolving aliases. + if ($new_command === $command) { + return $argv; + } + + $full_alias = idx($aliases, $command, array()); + $full_alias = implode(' ', $full_alias); + + // Run shell command aliases. + if (ArcanistAliasWorkflow::isShellCommandAlias($new_command)) { + fwrite( + STDERR, + tsprintf( + '** %s ** arc %s -> $ %s', + pht('ALIAS'), + $command, + $shell_cmd)); + + $shell_cmd = substr($full_alias, 1); + + return phutil_passthru('%C %Ls', $shell_cmd, $args); + } + + fwrite( + STDERR, + tsprintf( + '** %s ** arc %s -> arc %s', + pht('ALIAS'), + $command, + $new_command)); + + $new_argv = array_merge(array($new_command), $new_args); + + return $this->resolveAliases($workflows, $new_argv, $config); + } + + private function logTrace($label, $message) { + echo tsprintf( + "** %s ** %s\n", + $label, + $message); + } + +} diff --git a/scripts/phage.php b/scripts/phage.php deleted file mode 100755 index 1aa5c8f7..00000000 --- a/scripts/phage.php +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env php -parseStandardArguments(); - -$args->parsePartial(array()); - - -// TODO: This is pretty minimal and should be shared with "arc". -$working_directory = getcwd(); -$working_copy = ArcanistWorkingCopyIdentity::newFromPath($working_directory); -$config = id(new ArcanistConfigurationManager()) - ->setWorkingCopyIdentity($working_copy); - -foreach ($config->getProjectConfig('load') as $load) { - $load = Filesystem::resolvePath($working_copy->getProjectRoot().'/'.$load); - phutil_load_library($load); -} - - -$workflows = id(new PhutilClassMapQuery()) - ->setAncestorClass('PhageWorkflow') - ->execute(); -$workflows[] = new PhutilHelpArgumentWorkflow(); -$args->parseWorkflows($workflows);