From 0b5be45a7a57a6cc24f3a62889367656ff61ae4a Mon Sep 17 00:00:00 2001 From: Lexidor Digital <31805625+lexidor@users.noreply.github.com> Date: Tue, 13 Jul 2021 16:10:54 +0000 Subject: [PATCH] Pass through to facts (#72) * Documentation update Remove implied comparisons to composer autoloading. Remove statement about global namespace fallback. Document interactions with native autoloaders. Document HH\Facts mode (not present in this commit). * [Untested] Emit an HH\Facts autoload map May still need to lowercase the names. Will check in a Facts-enabled docker container. * Lowercase the names and call the right functions * Parens * Escape `HH\Facts`, no need for a random form feed * Rename useFactsIfAvailableAndDoNotEmitAStaticMap Now named useFactsIfAvailable * Support hhvm 4.109 and up Hhvm 4.116 and below need a HH_FIXME. Hhvm 4.117 has the hhi and the code typechecks on 4.117. * Update HH_FIXME messages The ext-facts hhi didn't make the cut for 4.117. --- .github/workflows/build-and-test.yml | 2 +- README.md | 24 ++++++++++--- bin/hh-autoload.hack | 50 ++++++++++++++++++++-------- composer.json | 2 +- src/Config.hack | 1 + src/ConfigurationLoader.hack | 10 ++++++ src/Writer.hack | 40 +++++++++++++++++++--- 7 files changed, 103 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d4ce8ae..154b896 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -13,7 +13,7 @@ jobs: matrix: os: [ ubuntu , macos ] hhvm: - - '4.67' + - '4.109' - latest - nightly runs-on: ${{matrix.os}}-latest diff --git a/README.md b/README.md index 488b511..861f7ce 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,11 @@ The autoloader for autoloading classes, enums, functions, typedefs, and constant Usage ===== -1. Add an `hh_autoload.json` file (see section below) and optionally remove your configuration from composer.json +1. Add an `hh_autoload.json` file (see section below) 2. `composer require hhvm/hhvm-autoload` -3. Replace any references to `vendor/autoload.php` with `vendor/autoload.hack` and call `Facebook\AutoloadMap\initialize()` -4. To re-generate the map, run `vendor/bin/hh-autoload`, `composer dump-autoload`, or any other command that generates the map +3. Require the autoload file from your entrypoint functions using `require_once (__DIR__ . '(/..)(repeat as needed)/vendor/autoload.hack');` +4. Call `Facebook\AutolaodMap\initialize()` to register the autoloader with hhvm. +5. To re-generate the map, run `vendor/bin/hh-autoload`, `composer dump-autoload`, or any other command that generates the map Configuration (`hh_autoload.json`) ================================== @@ -32,9 +33,10 @@ The following settings are optional: - `"includeVendor": false` - do not include `vendor/` definitions in `vendor/autoload.hack` - `"devRoots": [ "path/", ...]` - additional roots to only include in dev mode, not when installed as a dependency. - `"relativeAutoloadRoot": false` - do not use a path relative to `__DIR__` for autoloading. Instead, use the path to the folder containing `hh_autoload.json` when building the autoload map. - - `"failureHandler:" classname` - use the specified class to handle definitions that aren't the Map. Your handler will not be invoked for functions or constants - that aren't in the autoload map and have the same name as a definition in the global namespace. Defaults to none. + - `"failureHandler:" classname` - use the specified class to handle definitions that aren't the Map. Defaults to none. - `"devFailureHandler": classname` - use a different handler for development environments. Defaults to the same value as `failureHandler`. + - `"parser:" any of [ext-factparse]"` - select a parser to use, but there is only one valid option. Defaults to a sensible parser. + - `"useFactsIfAvailable": false` - use ext-facts (HH\Facts\...) to back Facebook\AutoloadMap\Generated\map() instead of a codegenned dict. See _Use with HH\Facts_ for more information about this mode. Use In Development (Failure Handlers) ===================================== @@ -90,12 +92,24 @@ Information you may need is available from: - `Facebook\AutoloadMap\Generated\root()`: the directory containing the project root, i.e. the parent directory of `vendor/` +Use with HH\\Facts +================= + +HHVM 4.109 introduced ext-facts and ext-watchman. Unlike the static pre-built autoloader which is built into a [repo authoratative](https://docs.hhvm.com/hhvm/advanced-usage/repo-authoritative) build, this native autoloader works incrementally and is suitable for autoloading in your development environment. For more information about setting up this autoloader, see the [blog post](https://hhvm.com/blog/2021/05/11/hhvm-4.109.html) for hhvm 4.109. + +When using a native autoloader (either the repo auth or ext-facts autoloader), you do not need hhvm-autoload to require classes/functions/types/constants at runtime. If you (and your vendor dependencies) do not call any functions in the `Facebook\AutoloadMap` namespace, other than `Facebook\AutoloadMap\initialize()`, you don't need hhvm-autoload anymore. In that case, you could drop this dependency and remove the calls to `initialize()`. If you are using other functions, like `Facebook\AutoloadMap\Generated\map()`, you'd still need the vendor/autoload.hack file that hhvm-autoload generates. + +Hhvm-autoload supports outputting a vendor/autoload.hack file which forwards all queries to ext-facts. `Facebook\AutoloadMap\Generated\map_uncached()` will always be up to date in this mode, since `HH\Facts` is always up to date. `Facebook\AutoloadMap\Generated\map()` is memoized (within a request), since some code may rely on getting the same result from multiple calls. You can enable this mode by adding `"useFactsIfAvailable": true` to the hh_autoload.json config file. Hhvm-autoload will emit a shim file instead of a full map. This option is ignored if `HH\Facts\enabled()` returns false, or when `--no-facts` is passed to `vendor/bin/hh-autoload`. We recommend passing `--no-facts` when building for production (specifically repo auth mode). Returning a hardcoded dict is faster than asking `HH\Facts`. + +Important to note. Autoloading with a native autoloader does not respect hh_autoload.json. The repo auth autoloader allows any code to use any symbol. The facts autoloader honors the configuration in .hhvmconfig.hdf instead. Make sure that the configuration in hh_autoload.json and .hhvmconfig.hdf match. + How It Works ============ - A parser (FactParse) provides a list of all Hack definitions in the specified locations - This is used to generate something similar to a classmap, except including other kinds of definitions - The map is provided to HHVM with [`HH\autoload_set_paths()`](https://docs.hhvm.com/hack/reference/function/HH.autoload_set_paths/) + - If a native autoloader is registered, this autoloader will intentionally not register itself. So calling `Facebook\AutoloadMap\initialize()` in repo auth mode or when the facts based autoloader is registered is a noop. Contributing ============ diff --git a/bin/hh-autoload.hack b/bin/hh-autoload.hack index 49ac25c..102ffd1 100755 --- a/bin/hh-autoload.hack +++ b/bin/hh-autoload.hack @@ -15,6 +15,7 @@ use namespace HH\Lib\Vec; final class GenerateScript { const type TOptions = shape( 'dev' => bool, + 'no-facts' => bool, ); private static function initBootstrapAutoloader(): void { @@ -94,22 +95,28 @@ final class GenerateScript { private static function parseOptions(vec $argv): self::TOptions { $options = shape( 'dev' => true, + 'no-facts' => false, ); $bin = $argv[0]; $argv = Vec\slice($argv, 1); foreach ($argv as $arg) { - if ($arg === '--no-dev') { - $options['dev'] = false; - continue; - } - if ($arg === '--help') { - self::printUsage(\STDOUT, $bin); - exit(0); + switch ($arg) { + case '--no-dev': + $options['dev'] = false; + break; + case '--no-facts': + $options['no-facts'] = true; + break; + case '--help': + self::printUsage(\STDOUT, $bin); + exit(0); + default: + \fprintf(\STDERR, "Unrecognized option: '%s'\n", $arg); + self::printUsage(\STDERR, $bin); + exit(1); } - \fprintf(\STDERR, "Unrecognized option: '%s'\n", $arg); - self::printUsage(\STDERR, $bin); - exit(1); } + return $options; } @@ -139,22 +146,37 @@ final class GenerateScript { $options['dev'] ? IncludedRoots::DEV_AND_PROD : IncludedRoots::PROD_ONLY, ); + $config = $importer->getConfig(); + $handler = $options['dev'] - ? ($importer->getConfig()['devFailureHandler'] ?? null) - : ($importer->getConfig()['failureHandler'] ?? null); + ? $config['devFailureHandler'] + : $config['failureHandler']; + + $emit_facts_forwarder_file = $config['useFactsIfAvailable'] && + !$options['no-facts']; (new Writer()) ->setBuilder($importer) ->setRoot(\getcwd()) - ->setRelativeAutoloadRoot($importer->getConfig()['relativeAutoloadRoot']) + ->setRelativeAutoloadRoot($config['relativeAutoloadRoot']) ->setFailureHandler(/* HH_IGNORE_ERROR[4110] */ $handler) ->setIsDev($options['dev']) + ->setEmitFactsForwarderFile($emit_facts_forwarder_file) ->writeToDirectory(\getcwd().'/vendor/'); print(\getcwd()."/vendor/autoload.hack\n"); } private static function printUsage(resource $to, string $bin): void { - \fprintf($to, "USAGE: %s [--no-dev]\n", $bin); + \fprintf( + $to, + "USAGE: %s [--no-dev] [--no-facts]\n". + "See the README for full information.\n". + "The README can be found at:\n\t- %s\n\t- %s.\n", + $bin, + \getcwd().'/vendor/hhvm/hhvm-autoload/README.md', + // ^^^^^^ Not accurate if hhvm-autoload is the top level project. + 'https://github.com/hhvm/hhvm-autoload/blob/master/README.md', + ); } private static function getFileList(string $root): vec { diff --git a/composer.json b/composer.json index 69fa2f6..01c4cd1 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ ], "require": { "composer-plugin-api": "^1.0|^2.0", - "hhvm": "^4.67", + "hhvm": "^4.109", "hhvm/hsl": "^4.0" }, "require-dev": { diff --git a/src/Config.hack b/src/Config.hack index 281e6c9..b7f0088 100644 --- a/src/Config.hack +++ b/src/Config.hack @@ -19,4 +19,5 @@ type Config = shape( 'failureHandler' => ?string, 'devFailureHandler' => ?string, 'relativeAutoloadRoot' => bool, + 'useFactsIfAvailable' => bool, ); diff --git a/src/ConfigurationLoader.hack b/src/ConfigurationLoader.hack index 60061c2..70b433f 100644 --- a/src/ConfigurationLoader.hack +++ b/src/ConfigurationLoader.hack @@ -85,6 +85,16 @@ abstract final class ConfigurationLoader { 'devFailureHandler', ) ?? $failure_handler, + 'useFactsIfAvailable' => ( + TypeAssert\is_nullable_bool( + $data['useFactsIfAvailable'] ?? null, + 'useFactsIfAvailable', + ) ?? + false + ) && + /* HH_FIXME[2049] Facts landed in 4.109, but the hhi landed in 4.118 */ + /* HH_FIXME[4107] Facts landed in 4.109, but the hhi landed in 4.118 */ + \HH\Facts\enabled(), ); } diff --git a/src/Writer.hack b/src/Writer.hack index 4e9c926..2740da4 100644 --- a/src/Writer.hack +++ b/src/Writer.hack @@ -26,6 +26,7 @@ final class Writer { private bool $relativeAutoloadRoot = true; private ?string $failureHandler; private bool $isDev = true; + private bool $emitFactsForwarderFile = false; /** Mark whether we're running in development mode. * @@ -55,6 +56,11 @@ final class Writer { return $this; } + public function setEmitFactsForwarderFile(bool $should_forward): this { + $this->emitFactsForwarderFile = $should_forward; + return $this; + } + /** Set the files and maps from a builder. * * Convenience function; this is equivalent to calling `setFiles()` and @@ -166,9 +172,30 @@ final class Writer { true, ); - $map = \var_export($map, true) - |> \str_replace('array (', 'dict[', $$) - |> \str_replace(')', ']', $$); + if (!$this->emitFactsForwarderFile) { + $memoize = ''; + $map_as_string = \var_export($map, true) + |> \str_replace('array (', 'dict[', $$) + |> \str_replace(')', ']', $$); + } else { + $memoize = "\n<<__Memoize>>"; + $map_as_string = <<<'EOF' +dict[ + /* HH_FIXME[2049] Facts landed in 4.109, but the hhi landed in 4.118 */ + /* HH_FIXME[4107] Facts landed in 4.109, but the hhi landed in 4.118 */ + 'class' => \HH\Lib\Dict\map_keys(\HH\Facts\all_types(), \HH\Lib\Str\lowercase<>), + /* HH_FIXME[2049] Facts landed in 4.109, but the hhi landed in 4.118 */ + /* HH_FIXME[4107] Facts landed in 4.109, but the hhi landed in 4.118 */ + 'function' => \HH\Lib\Dict\map_keys(\HH\Facts\all_functions(), \HH\Lib\Str\lowercase<>), + /* HH_FIXME[2049] Facts landed in 4.109, but the hhi landed in 4.118 */ + /* HH_FIXME[4107] Facts landed in 4.109, but the hhi landed in 4.118 */ + 'type' => \HH\Lib\Dict\map_keys(\HH\Facts\all_type_aliases(), \HH\Lib\Str\lowercase<>), + /* HH_FIXME[2049] Facts landed in 4.109, but the hhi landed in 4.118 */ + /* HH_FIXME[4107] Facts landed in 4.109, but the hhi landed in 4.118 */ + 'constants' => \HH\Facts\all_constants(), + ] +EOF; + } if ($this->relativeAutoloadRoot) { try { @@ -206,9 +233,12 @@ function is_dev(): bool { return (bool) \$override; } +function map_uncached(): \Facebook\AutoloadMap\AutoloadMap { + return $map_as_string; +} +$memoize function map(): \Facebook\AutoloadMap\AutoloadMap { - /* HH_IGNORE_ERROR[4110] invalid return type */ - return $map; + return map_uncached(); } } // Generated\