Nanoc needs file paths to be known before compilation starts

Last edited March 2021

In Nanoc, routing strictly happens before compilation. This prevents an output filename from depending on the compiled content.

Example: Fingerprinting

This is limiting particularly for stylesheet fingerprinting. Ideally, each stylesheet contains the hash of its own compiled content in the filename, but Nanoc is only able to calculate a hash from the raw, uncompiled input.

As an example, here is how the nanoc.ws web site handles fingerprinting for stylesheets:

compile '/assets/style/*' do
  filter :sass, syntax: :scss
  write item.identifier.without_ext + '-' + fingerprint('/assets/style/*') + '.css'
end
def fingerprint(pattern)
  contents = @items.find_all(pattern).map do |i|
    i.binary? ? File.read(i[:filename]) : i.raw_content
  end
  Digest::SHA1.hexdigest(contents.join(''))[0..15]
end

The implementation of #fingerprint looks icky, but there is a bigger issue.

If multiple stylesheets exist (e.g. /assets/style/screen.scss and /assets/style/print.scss), their filenames will depend on all partials and all non-partials in the /assets/style directory, which might cause a stylesheet’s fingerprint to change even though their contents haven’t changed.

Causes

For each item, Nanoc runs several outdatedness rules, which determine whether or not the item is outdated and thus needs to be recompiled. These outdatedness rules answer questions such as these:

If none of these outdatedness rules respond with “yes,” the item is not considered to be outdated, and will not be recompiled.

The last mentioned outdatedness rule in the list above is of interest: if the corresponding output file does not exist, Nanoc will consider the item as outdated, and compile it.

This is why Nanoc needs to know the file path before compiling the item.

Workaround

A dependency-querying API would help for generating fingerprints that are taken from only the necessary items. In the stylesheet fingerprinting example above, the fingerprint(glob) call would become fingerprint(item), and @items.find_all(pattern) would become something like item.dependencies.

Such a dependency-querying API wouldn’t solve the underlying problem of not being able to generate a fingerprint from the compiled content, however.

Resolution

To do: Probably solvable if Nanoc uses an internal output databases that relies on item identifiers (or arbitrarily-assigned UUIDs) rather than paths. CoW would then help with reducing filesystem usage.

References

GitHub. “Cache Fingerprinting Doesn’t Take Account of Change to Non-Partial · Issue #251 · nanoc/nanoc.ws.” Accessed January 12, 2021. https://github.com/nanoc/nanoc.ws/issues/251.