Getting Started: Starting A Real Project
Starting A Real Project
Real projects generally aren't a single file in isolation; they tend to have dependencies such as the Hack Standard Library, and various optional tools.
A good starting point is to:
- install Composer
- create an
.hhconfig
file - create
src/
andtests/
subdirectories - configure autoloading
- use Composer to install the common dependencies and tools
Autoloading
In HHVM, there is no 'build' step as such; each file is processed as needed.
Currently, HHVM needs to be given a map of what files define which classes,
functions and so on - for example, to execute the code new Foo()
, HHVM needs
to know that the class Foo
is defined in src/Foo.hack
.
hhvm-autoload generates this map. To add it to your project, run:
$ php /path/to/composer.phar require hhvm/hhvm-autoload
hhvm-autoload needs an hh_autoload.json
configuration
file. For most projects, a minimal example is:
{
"roots": [
"src/"
],
"devRoots": [
"tests/"
],
"devFailureHandler": "Facebook\\AutoloadMap\\HHClientFallbackHandler"
}
The "roots" key provides folders that need to be loadable in a production environment.
The "devRoots" key is for folders that you want to be autoloaded during development or testing, but not when you are running your code in production.
The "devFailureHandler" key is the fully qualified name of a fallback strategy.
When you add a new class or function and don't run hh-autoload
, the autoloadmap is not automatically updated.
The fallback is called when hhvm can't find your type, constant or function in the autoloadmap.
The fallback then may attempt to load the type, constant or function at runtime. (This process will slow down your execution considerably and should therefore not used in production.) Not all constants and functions can / will be found by HHClientFallbackHandler, see the repository for more details.
Once this configuration file is created, vendor/bin/hh-autoload
can be executed
to generate or update the map, which is created as vendor/autoload.hack
An Example
The following sequence of commands could be used to fully initialize a Hack project with the most common dependencies:
$ curl https://raw.githubusercontent.com/hhvm/hhast/master/.hhconfig > .hhconfig
$ mkdir bin src tests
$ cat > hh_autoload.json
{
"roots": [
"src/"
],
"devRoots": [
"tests/"
],
"devFailureHandler": "Facebook\\AutoloadMap\\HHClientFallbackHandler"
}
$ composer require hhvm/hsl hhvm/hhvm-autoload
$ composer require --dev hhvm/hhast hhvm/hacktest facebook/fbexpect
You may need to use the full path to Composer, depending on how it's installed.
We curl an existing hhconfig from hhast from github. The reason for this is that starting with hhvm version 4.62, it is no longer enough for projects that use external dependencies. Almost all packages you pull in using composer will have a suppression comment in them somewhere. You must whitelist these suppression comments in order to use these packages.
The hhast .hhconfig
file whitelists all suppression comments used by hsl, hhvm-autoload, hacktest, fbexpect, hhast. Hhast depends on these packages itself, so this should stay up to date. If the result of hh_client restart && hh_client
does not end with No errors!
after the last step, please refer to the error suppression docs.
The same commands with their expected output:
$ curl https://raw.githubusercontent.com/hhvm/hhast/master/.hhconfig > .hhconfig
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
xxx xxx xxx xxx x x xxxx x --:--:-- --:--:-- --:--:-- xxxx
$ mkdir bin src tests
$ cat > hh_autoload.json
{
"roots": [
"src/"
],
"devRoots": [
"tests/"
],
"devFailureHandler": "Facebook\\AutoloadMap\\HHClientFallbackHandler"
}
$ composer require hhvm/hsl hhvm/hhvm-autoload
Using version ^4.0 for hhvm/hsl
Using version ^2.0 for hhvm/hhvm-autoload
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
- Installing hhvm/hsl (v4.0.0): Loading from cache
- Installing hhvm/hhvm-autoload (v2.0.3): Loading from cache
Writing lock file
Generating autoload files
/var/folders/3l/2yk1tgkn7xdd76bs547d9j90fcbt87/T/tmp.xaQwE1xE/vendor/autoload.hack
$ composer require --dev hhvm/hhast hhvm/hacktest facebook/fbexpect
Using version ^4.0 for hhvm/hhast
Using version ^1.4 for hhvm/hacktest
Using version ^2.5 for facebook/fbexpect
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 7 installs, 0 updates, 0 removals
- Installing facebook/difflib (v1.1): Loading from cache
- Installing hhvm/hsl-experimental (v4.0.1): Loading from cache
- Installing hhvm/type-assert (v3.3.1): Loading from cache
- Installing facebook/hh-clilib (v2.1.0): Loading from cache
- Installing hhvm/hhast (v4.0.4): Loading from cache
- Installing hhvm/hacktest (v1.4): Loading from cache
- Installing facebook/fbexpect (v2.5.1): Loading from cache
Writing lock file
Generating autoload files
/private/var/folders/3l/2yk1tgkn7xdd76bs547d9j90fcbt87/T/tmp.xaQwE1xE/vendor/autoload.hack
Adding Functions or Classes
As a toy example, we're going to create a function that squares a vector of
numbers; save the following as src/square_vec.hack
:
use namespace HH\Lib\Vec;
function square_vec(vec<num> $numbers): vec<int> {
return Vec\map($numbers, $number ==> $number * $number);
}
If you then run hh_client
, it will tell you of a mistake:
src/square_vec.hack:4:10,57: Invalid return type (Typing[4110])
src/square_vec.hack:3:53,55: This is an int
src/square_vec.hack:4:40,56: It is incompatible with a num (int/float) because this is the result of an arithmetic operation with a num as the first argument, and no floats.
src/square_vec.hack:3:35,35: Here is why I think the argument is a num: this is a num
To fix this, change the return type of the function from vec<int>
to vec<num>
.
We now have a function that is valid Hack, but it's not tested, and nothing calls it.
Adding an Executable
Save the following as bin/square_some_things.hack
:
#!/usr/bin/env hhvm
<<__EntryPoint>>
async function main(): Awaitable<void> {
require_once(__DIR__.'/../vendor/autoload.hack');
\Facebook\AutoloadMap\initialize();
$squared = square_vec(vec[1, 2, 3, 4, 5]);
foreach ($squared as $square) {
printf("%d\n", $square);
}
}
This program:
- requires and initializes the autoloader so that the function we just defined can be found
- calls the function
- prints the results
You can now execute your new program, either explicitly with HHVM, or by marking it as executable:
$ hhvm bin/square_some_things.hack
1
4
9
16
25
$ chmod +x bin/square_some_things.hack
$ bin/square_some_things.hack
1
4
9
16
25
Linting
Most projects use a linter to enforce some stylistic choices that are not
required by the language, but help make the project consistent; HHAST is the
recommended linter for Hack code. HHAST's linter is enabled by an
hhast-lint.json
file in the project root. A good starting project is to enable
all linters for all directories that contain source code - to do this, save
the following as hhast-lint.json
:
{
"roots": [ "bin/", "src/", "tests/" ],
"builtinLinters": "all"
}
When you ran composer require
earlier, HHAST was installed into the vendor/
subdirectory, and can be executed from there:
$ vendor/bin/hhast-lint
Function "main()" does not match conventions; consider renaming to "main_async"
Linter: Facebook\HHAST\Linters\AsyncFunctionAndMethodLinter
Location: /private/var/folders/3l/2yk1tgkn7xdd76bs547d9j90fcbt87/T/tmp.xaQwE1xE/bin/square_some_things.hack:5:0
Code:
>
><<__EntryPoint>>
>async function main(): Awaitable<void>
Unit Testing
HackTest is used to create unit test classes, and fbexpect is used to
express assertions. Let's create a basic test as tests/MyTest.hack
:
use function Facebook\FBExpect\expect;
use type Facebook\HackTest\{DataProvider, HackTest};
final class MyTest extends HackTest {
public function provideSquaresExamples(): vec<(vec<num>, vec<num>)> {
return vec[
tuple(vec[1, 2, 3], vec[1, 4, 9]),
tuple(vec[1.1, 2.2, 3.3], vec[1.1 * 1.1, 2.2 * 2.2, 3.3 * 3.3]),
];
}
<<DataProvider('provideSquaresExamples')>>
public function testSquares(vec<num> $in, vec<num> $expected_output): void {
expect(square_vec($in))->toBeSame($expected_output);
}
}
We can then use HackTest to run the tests:
$ vendor/bin/hh-autoload
$ vendor/bin/hacktest tests/
..
Summary: 2 test(s), 2 passed, 0 failed, 0 skipped, 0 error(s).
Regenerating the autoloadmap (with hh-autoload) is not always required, but if classes are not in the autoloadmap, you may get exceptions about reflected classes not existing. It is generally recommended to make sure that the autoloadmap is complete, before running the test suite.
If we intentionally add a failure, such as tuple(vec[1, 2, 3], vec[1, 2, 3])
,
HackTest reports this:
$ vendor/bin/hacktest tests/
..F
1) MyTest::testSquares with data set #3 (vec [
1,
2,
3,
], vec [
1,
2,
3,
])
Failed asserting that vec [
1,
4,
9,
] is the same as vec [
1,
2,
3,
]
/private/var/folders/3l/2yk1tgkn7xdd76bs547d9j90fcbt87/T/tmp.xaQwE1xE/tests/MyTest.hack(15): Facebook\FBExpect\ExpectObj->toBeSame()
Summary: 3 test(s), 2 passed, 1 failed, 0 skipped, 0 error(s).
Configuring Git
The vendor/
directory should not be committed; to make dependencies available
on another system or checkout, use composer install
. This will use the
generated composer.lock
file (which should generally be committed) to install
the exact same versions.
$ echo vendor/ > .gitignore
If you're creating a library, users of your library probably don't want your
unit tests - and if they have them, they will need to have fbexpect
and
hacktest
installed in compatible versions to not get Hack errors.
As Composer uses GitHub releases which are automatically generated via
git export
, the simplest solution is to configure git export
to ignore
the tests/
directory:
$ echo 'tests/ export-ignore' > .gitattributes
Configuring TravisCI
We recommend using Docker on TravisCI for continuous integration of Hack
projects. This is usually done by creating a separate .travis.sh
which
executes in the container. For example, a .travis.yml
might look like this:
sudo: required
language: generic
services: docker
env:
- HHVM_VERSION=latest
- HHVM_VERSION=nightly
install:
- docker pull hhvm/hhvm:$HHVM_VERSION
script:
- docker run --rm -w /var/source -v $(pwd):/var/source hhvm/hhvm:$HHVM_VERSION ./.travis.sh
... and a corresponding .travis.sh
:
#!/bin/sh
set -ex
apt update -y
DEBIAN_FRONTEND=noninteractive apt install -y php-cli zip unzip
hhvm --version
php --version
(
cd $(mktemp -d)
curl https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
)
composer install
hh_client
vendor/bin/hacktest tests/
if !(hhvm --version | grep -q -- -dev); then
vendor/bin/hhast-lint
fi
With this configuration, TravisCI runs will check for hack errors, unit test
failures - and on release builds, run hhast-lint
. We do not run hhast-lint
on -dev
builds as hhast-lint
depends on implementation details of HHVM and
Hack which change frequently.