Skip to content

Shinmera/forge

Repository files navigation

[ image logo.svg ]
# AAAAAAAAAAAAAAAAAAAAAAAAa
This is still in flux

## User Manual
This section describes an overview on how to use Forge to perform builds and set your project up to build with it. Do note however that since Forge is a generic build system, you will also need to refer to documentation for the specific kind of project you're building to get the full picture.

### Building
If you have ``forge`` in your path, performing the default build can be done simply by invoking ``forge`` from a directory that contains a ``blueprint`` file. It will automatically discover the blueprint and run the default build task.

You can discover more about forge's command line arguments by running ``forge help``, which among other information will show you all the possible subcommands, including each command's arguments. Nevertheless, here's some other common uses:

- ``forge -c ~/path/to/project/``
  Invoke forge from a different directory than the blueprint file.
- ``forge -t project``
  Execute the default effect for ``project``, discovering the project's location automatically.
- ``forge effect parameter...``
  Execute a plan to reach the described effect.
- ``forge --force``
  Force the plan execution to disregard caching.

By default Forge will try to discover a running server and use that as build host if it can be found. If not, it will act as server and spin up a local client to perform the build. Please see the configuration section on how to change this behaviour.  

### Writing Blueprints
In order to write project descriptions, you have to create a file called ``blueprint`` and place it somewhere that Forge can find it. A blueprint file can define a number of projects at once, and can potentially be located anywhere in relation to other source files or components necessary for the build.

What exactly the blueprint file should contain will depend on the type of projects you are defining, but generally each project definition will look something like this:

::
(forge:define-project (project-type)
  :name "project-name"
  :version 0
  :components ("some-file.x" "*.thing"))
::

A more rigorous specification of the grammar is as follows:

::
definition   ::= (forge:define-project (module+) karg*)
karg         ::= name
               | version
               | components
               | project-type-specific-argument
name         ::= :name string
version      ::= :version version-designator
components   ::= :components (component-specification*)
module       --- A symbol naming a module that needs to be loaded to parse this project
                 definition. The first module specified also determines the type of project
                 that is defined.
project-type-specific-argument
             --- An argument specific to the project type used. However, each argument still
                 follows the key value format.
::

Note that most of the definition will be specific to the type of project you're defining, so please refer to that specific module's documentation on how exactly to write a project definition.

#### Version Designators
Forge supports a number of different version specification schemes:

- **Hash**
  A simple string that designates a hash of a version. If you use a hash, Forge will not be able to determine whether a version is newer or older than another.
- **Integer**
  A single, increasing positive integer version. There's no constraints on the range of the integer.
- **Separated**
  A list of integer versions, in descending order of importance. Meaning a version of ``(1 2 3)`` is newer than ``(1 3 2)``.
- **Compound**
  A combination of multiple of the previous schemes, again with the earlier scheme taking precedence for the ordering.

Each of these versions can also be parsed from a string if you prefer to specify them that way:

- ``"1"`` - Integer
- ``"1.2"`` - Separated
- ``"1.2-a"`` - Compound (Separated + Hash)
- ``"1.2-32"`` - Compound (Separated + Integer)
- ``"a"`` - Hash

Additional modules may extend the types of versions supported.

#### Component Specifications
Each component can be specified as either a singular pattern for files to match, or as a list of the following structure:

::
component ::= (name karg*)
karg      ::= type
            | component-type-specific-argument
type      ::= :type component-type-name
name      --- A string naming the component. Must be unique within the project.
component-type-specific-argument
          --- An argument specific to the component type used. However, each argument still
              follows the key value format.
component-type-name
          --- A symbol naming the type of component to construct. If not specified, a default
              type will be chosen based on the project type.
::

Again, most of the arguments permissible will be specific to the type of component used. Please refer to the appropriate documentation for further instructions.

### Configuration
; TODO

## Internals
This section discusses the internals of the Forge system. You should read this if you plan to contribute to Forge itself or would like to extend it in some way, be that by supporting a new language target, or adding other support functionality.

### Concepts
Forge employs a strict separation model in two ways:

1. The planning phase is required to be entirely deterministic and **must not** touch the file system or other variable parts. A fully generated plan can then be executed via a number of different execution strategies, at which point steps in the plan may be elided to avoid unnecessary work, and may be distributed across multiple clients that perform the work.
2. Execution should not happen on the same process as planning and management. This ensures that side-effects from plan execution are isolated from the planner, and also that needed libraries for planning and such are not needed on an executing client.

Being separated as such, we will go over the concepts in separation as well, first looking at planning, then at execution, and finally at the distribution and file system aspects.

#### Planning
Plans are oriented around ``effect``s, which are an abstract description of some kind of change that we would like to achieve. Effects are caused by ``perform``ing some kind of ``operation`` on some kind of ``compononent``. Both operations, components, and effects are entirely abstract in this, though they will often be used to represent something like: performing compilation on file A produces a fragment B, with the B output being the effect, file A being the component, and compilation being the operation.

An effect may be produced by different combinations of operations and components. To resolve this ambiguity, computing a plan takes a ``policy`` object. The policy has effects both on the selection of which effective operation and component are used to achieve any effect, and they also influence the exact parameters of each operation object, allowing the influence of things such as compilation parameters.

Any operation and component pair will have a list of ``dependencies``, each of which should be a ``dependency`` object that describes applicable effects that, if achieved, satisfy the dependency. In order to perform the operation on the component, all hard dependencies must first be resolved. A dependency may also be weak, in which case the dependent effect must only be achieved first if it is already included elsewhere as a hard dependency.

In order to resolve a dependency, a database is first searched for effects that match the dependency's ``effect-type``, ``parameters``, and ``version`` constraints. Possible effects are then gathered into sets of effects across the whole plan. Once all possible sets are computed, a single effects set is selected via the policy using ``select-effect-set``.

Once the graph of effects has been computed, it is transformed into a graph of ``steps`` to form a ``plan``. Each ``step`` retains the exact effect, operation, and component selected by the planner, and holds a list of ``predecessors`` and ``successors`` steps.

#### Execution
Since a ``plan`` is completely independent of any filesystem or client, before a ``plan`` can be executed, it must first be ``realize``d. During realization, the set of clients that will execute steps is selected, individual steps are allocated to the clients, and the steps are transformed into ``local-step``s, which contain information that is localised to a specific client.

During realization the ``executor`` may also already mark steps as ``complete``, if it can determine that the client has performed the operation before and can instead re-use a cached version.

One important detail is that ``execute`` and ``perform`` both must return a ``promise`` object, and the executor must ensure that ``perform`` is not called eagerly or synchronously, as the communication with clients requires that the execution of a step happen asynchronously.

About

A general, modular build system.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published