PSA: Use a project for building your docs

documentation

#1

This is a simple guide on how to best build your documentation in Julia v1.0 if you are using Documenter.

The old way

If you are using Documenter you probably have something like the following in your .travis.yml file for building your documentation:

after_success:
  - julia -e 'Pkg.add("Documenter")'
  - julia -e 'cd(Pkg.dir("PACKAGE_NAME")); include(joinpath("docs", "make.jl"))'

Why is this bad? Here are some reasons:

  • There is no good way to add doc-build dependencies, you have to add them manually (like we Pkg.add("Documenter") above).
  • Code in the after_success: part of the build does not fail the build. This makes it difficult to verify that (i) the doc build works and (ii) that doctest etc. still passes. For this reason some package have chosen to build the docs as part of the test suite instead.
  • Doc building runs in the global environment, and is thus affected by the surroundings.

So, maybe we can use this new Pkg thing, that apparently should solve all our problems. Absolutely!

The new way

In Julia v1.0 we can instead use a designated doc-build project with a Project.toml in the docs/ directory. The project includes Documenter and all your other doc-build dependencies. If Documenter is the only dependency, then the Project.toml file should include the following:

[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"

[compat]
Documenter = "~0.19"

Here we also added a compatibility constraint for Documenter, since I happen to know that Documenter 0.20 will break everything.

Instead of creating the Project.toml file manually we could have started Julia with julia --project=docs/ (alternatively just start Julia and then using Pkg; Pkg.activate("docs")) and then used the Pkg REPL to add our doc-build dependencies.

Next we need to configure the .travis.yml to use this project. For this we will utilize Travis Build Stages that have just been released (see https://docs.travis-ci.com/user/build-stages). Add the following to your .travis.yml file:

jobs:
  include:
    - stage: "Documentation"
      julia: 1.0
      os: linux
      script:
        - julia --project=docs/ -e 'using Pkg; Pkg.instantiate();
                                    Pkg.add(PackageSpec(path=pwd()))'
        - julia --project=docs/ docs/make.jl
      after_success: skip

This adds a new build stage, called Documentation, that will run after the default test stage. It will look something like this. The julia: and os: entries control from which worker we build the docs, for example Julia 1.0 and linux. What happens on the three lines in the script: part?

  1. The first line Pkg.instantiate()s the project, meaning that Documenter and the other dependencies will be installed.
  2. The second line adds our package to the doc-build environment.
  3. The third line builds the docs.

Lastly, commit the files (the Manifest.toml can be .gitignored) and push!

Why is this better?

  • Using a custom docs projects gives full control over the environment where the docs build.
  • We can have documentation dependencies.
  • A failed doc build now fails CI.

In my opinion this is a pretty nice example of how to utilize the new Pkg environments.

If you need some inspiration, have a look at the following packages that already uses projects and build stages for doc-building:

Some gotchas

  1. Make sure that the julia and os arguments to Documenter.deploydocs match the configuration you define for the build stage in .travis.yml. Note that this is something that will change in the next Documenter release, where the julia and os arguments are removed and the deployment is essentially governed by the .travis.yml configuration. So the annoyance of keeping those in sync is temporary.

  2. You might run into https://github.com/JuliaLang/julia/pull/28625 which is a bug in Julia v0.7, v1.0 (but fixed on nightly and the upcoming Julia v1.0.1). You can add the following lines at the top of docs/make.jl to workaround it:

    # Workaround for JuliaLang/julia/pull/28625
    if Base.HOME_PROJECT[] !== nothing
        Base.HOME_PROJECT[] = abspath(Base.HOME_PROJECT[])
    end
    

Feel free to ping me (@fredrikekre) for any questions from your PRs. Happy upgrading!


#3

Awesome, thanks!

Could this be integrated into the default travis build script, so that it just automatically runs if there is a docs/make.jl file?


#4

I am pretty sure the Julia Travis script can’t create a new build stage. Also, you need need to provide explicit Julia version and OS information to the build stage.


#5

since I happen to know that Documenter 0.20 will break everything.

This is persuading me to leave everything as it is for now, and address all the breakages and upgrades at the same time…


#6

Just to clarify: The next Documenter release will not break the setup I am suggesting here. It will however require some changes to the docs/make.jl file. Actually, adding the compatibility constraint like I am suggesting will protect you from the breakage, since if you blindly Pkg.add("Documenter") you will get the breaking version when that is released.


#7

It should be noted that if Manifest.toml is checked in then the exact version of those packages will be used every time the documentation is built. This means that generating the documentation is reproducible and isn’t at risk of breaking just because someone released a new (perhaps broken) version of one of your documentation dependencies.


#8

Right, maybe that is actually a better option but then you have to explicitly upgrade your doc build dependencies to get shiny new stuff. I actually do this in Literate.jl, where I have developed the package with a relative path, stored in the manifest. That means that Pkg.instantiate() is enough, and I don’t have to Pkg.add my own package as a separate step.


#9

Thanks @fredrikekre! Makes doc building more reliable and reproducible.

Unfortunately it means that the package has to be built one more time, which was not the case with the old approach. Do you know if there is a way around? It feels a bit like a waste of CI resources, since it was built already.