Skip to content

Commit

Permalink
rewrite the build system in Haskell
Browse files Browse the repository at this point in the history
Heavily inspired by the 1lab's build system, but nothing too fancy.
This is a pile of hacks, please don't look.
I just copied Agda's HTML backend and modified it so that links to
external projects link to their respective websites.
  • Loading branch information
ncfavier committed Jul 5, 2024
1 parent 98431fb commit 502a11d
Show file tree
Hide file tree
Showing 11 changed files with 947 additions and 54 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ result
result-*
*.agdai
MAlonzo/**
.shake
dist-newstyle
_build
1 change: 1 addition & 0 deletions cabal.project
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages: shake
2 changes: 1 addition & 1 deletion cubical-experiments.agda-lib
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: cubical-experiments
include: src
include: src _build
depend:
cubical
1lab
Expand Down
73 changes: 21 additions & 52 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -11,65 +11,34 @@
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
inherit (nixpkgs) lib;
myAgda = pkgs.agda.withPackages (p: with p; [
cubical
_1lab
]);
AGDA_LIBRARIES_FILE = pkgs.writeText "libraries"
(lib.concatMapStrings (p: "${p}/${p.libraryFile}\n") (with pkgs.agdaPackages; [
cubical
_1lab
]));
shakefile = pkgs.haskellPackages.callCabal2nix "cubical-experiments-shake" ./shake {};
in {
devShells.${system}.shakefile = pkgs.haskellPackages.shellFor {
packages = _: [ shakefile ];
inherit AGDA_LIBRARIES_FILE;
};

packages.${system}.default = pkgs.stdenv.mkDerivation {
name = "cubical-experiments";
src = self;
nativeBuildInputs = with pkgs; [
(agda.withPackages (p: with p; [
cubical
_1lab
]))
htmlq
nativeBuildInputs = [
myAgda
shakefile
];
inherit AGDA_LIBRARIES_FILE;
LC_ALL = "C.UTF-8";
postHead = ''
<meta name="author" content="Naïm Favier">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
<link rel="icon" type="image/png" href="//monade.li/favicon.png">
'';
preBodyInternal = ''
<h3>
<a href="/">index</a> ∙
<a href="https://github.com/ncfavier/cubical-experiments/blob/main/@MODULE@">source</a>
</h3>
'';
preBodyExternal = ''
<h3><a href="/">index</a></h3>
<p style="border-left: 5px solid #facc15; padding: 1rem;">
⚠️ This module is not part of this project and is only included for reference.<br>
It is either part of the <a href="https://1lab.dev">1lab</a>, the <a href="https://github.com/agda/cubical">cubical</a> library, or a built-in Agda module.
</p>
'';
buildPhase = ''
shopt -s extglob nullglob
for f in src/*.agda src/*.lagda*; do
base=''${f##*/} mod=''${base%%.@(agda|lagda*)}
if grep -qE 'import *(1Lab|Cat)\.' "$f"; then
echo "import $mod" >> Everything-1Lab.agda
else
echo "import $mod" >> Everything.agda
fi
done
agda -i . --html --html-dir="$out" --html-highlight=all --css=style.css --highlight-occurrences Everything.agda
agda -i . --html --html-dir="$out" --html-highlight=all --css=style.css --highlight-occurrences Everything-1Lab.agda
for f in "$out"/*.html; do
base=''${f##*/} mod=''${base%%.html}
printf -v mod %b "''${mod//%/'\x'}"
src=(src/"$mod".@(agda|lagda*))
if (( ''${#src} )); then
substituteInPlace "$f" \
--replace-fail '</head>' "$postHead</head>" \
--replace-fail '<body>' "<body>''${preBodyInternal/@MODULE@/"''${src[0]}"}"
else
substituteInPlace "$f" \
--replace-fail '</head>' "$postHead</head>" \
--replace-fail '<body>' "<body>$preBodyExternal"
fi
done
cp ${self}/{index.html,style.css} "$out"/
modules=$(for e in Everything-1Lab Everything; do htmlq pre.Agda -f "$out/$e.html"; done)
substituteInPlace "$out"/index.html --subst-var-by modules "$modules"
cubical-experiments-shake
mv _build/site "$out"
'';
};
};
Expand Down
3 changes: 2 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</head>
<body>
<h1><a href="https://github.com/ncfavier/cubical-experiments">cubical-experiments</a></h1>
@modules@
<pre class="Agda">
@agda@</pre>
</body>
</html>
20 changes: 20 additions & 0 deletions module.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="author" content="Naïm Favier">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
<title>@moduleName@</title>
<link rel="icon" type="image/png" href="//monade.li/favicon.png">
<link rel="stylesheet" href="style.css">
<script type="text/javascript" src="highlight-hover.js"></script>
</head>
<body>
<h3>
<a href="/">index</a>
<a href="https://github.com/ncfavier/cubical-experiments/blob/main/src/@filename@">source</a>
</h3>
<pre class="Agda">
@agda@</pre>
</body>
</html>
223 changes: 223 additions & 0 deletions shake/HTML/Backend.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
{-# OPTIONS_GHC -Wunused-imports #-}

-- | Backend for generating highlighted, hyperlinked HTML from Agda sources.

module HTML.Backend
( htmlBackend
, htmlBackend'
, HtmlFlags(..)
, initialHtmlFlags
) where

import HTML.Base

import Prelude hiding ((!!), concatMap)

import Control.DeepSeq
import Control.Monad.Trans ( MonadIO )
import Control.Monad.Except ( MonadError(throwError) )

import qualified Data.Map as Map
import Data.Map (Map)

import GHC.Generics (Generic)

import Agda.Interaction.Options
( ArgDescr(ReqArg, NoArg)
, OptDescr(..)
, Flag
)
import Agda.Compiler.Backend (Backend(..), Backend'(..), Recompile(..))
import Agda.Compiler.Common (IsMain(..), curIF)

import Agda.Syntax.TopLevelModuleName (TopLevelModuleName, TopLevelModuleName' (TopLevelModuleName))

import Agda.TypeChecking.Monad
( MonadDebug
, ReadTCState
, Definition
, reportS, stModuleToSource, useTC, TCM, getAgdaLibFiles
)
import Agda.Interaction.Library.Base (_libName, LibName)
import Agda.Utils.FileName (AbsolutePath)
import Agda.Utils.Maybe (listToMaybe)
import Control.Monad ((<=<))

-- | Options for HTML generation

data HtmlFlags = HtmlFlags
{ htmlFlagEnabled :: Bool
, htmlFlagDir :: FilePath
, htmlFlagHighlight :: HtmlHighlight
, htmlFlagHighlightOccurrences :: Bool
, htmlFlagCssFile :: Maybe FilePath
} deriving (Eq, Generic)

instance NFData HtmlFlags

data HtmlCompileEnv = HtmlCompileEnv
{ htmlCompileEnvOpts :: HtmlOptions
, htmlModuleToURL :: ModuleToURL
}

data HtmlModuleEnv = HtmlModuleEnv
{ htmlModEnvCompileEnv :: HtmlCompileEnv
, htmlModEnvName :: TopLevelModuleName
}

data HtmlModule = HtmlModule
data HtmlDef = HtmlDef

htmlBackend :: Backend
htmlBackend = Backend htmlBackend'

htmlBackend' :: Backend' HtmlFlags HtmlCompileEnv HtmlModuleEnv HtmlModule HtmlDef
htmlBackend' = Backend'
{ backendName = "HTML"
, backendVersion = Nothing
, options = initialHtmlFlags
, commandLineFlags = htmlFlags
, isEnabled = htmlFlagEnabled
, preCompile = preCompileHtml
, preModule = preModuleHtml
, compileDef = compileDefHtml
, postModule = postModuleHtml
, postCompile = postCompileHtml
-- --only-scope-checking works, but with the caveat that cross-module links
-- will not have their definition site populated.
, scopeCheckingSuffices = True
, mayEraseType = const $ return False
}

initialHtmlFlags :: HtmlFlags
initialHtmlFlags = HtmlFlags
{ htmlFlagEnabled = False
, htmlFlagDir = defaultHTMLDir
, htmlFlagHighlight = HighlightAll
-- Don't enable by default because it causes potential
-- performance problems
, htmlFlagHighlightOccurrences = False
, htmlFlagCssFile = Nothing
}

htmlOptsOfFlags :: HtmlFlags -> HtmlOptions
htmlOptsOfFlags flags = HtmlOptions
{ htmlOptDir = htmlFlagDir flags
, htmlOptHighlight = htmlFlagHighlight flags
, htmlOptHighlightOccurrences = htmlFlagHighlightOccurrences flags
, htmlOptCssFile = htmlFlagCssFile flags
}

-- | The default output directory for HTML.

defaultHTMLDir :: FilePath
defaultHTMLDir = "html"

htmlFlags :: [OptDescr (Flag HtmlFlags)]
htmlFlags =
[ Option [] ["html"] (NoArg htmlFlag)
"generate HTML files with highlighted source code"
, Option [] ["html-dir"] (ReqArg htmlDirFlag "DIR")
("directory in which HTML files are placed (default: " ++
defaultHTMLDir ++ ")")
, Option [] ["highlight-occurrences"] (NoArg highlightOccurrencesFlag)
("highlight all occurrences of hovered symbol in generated " ++
"HTML files")
, Option [] ["css"] (ReqArg cssFlag "URL")
"the CSS file used by the HTML files (can be relative)"
, Option [] ["html-highlight"] (ReqArg htmlHighlightFlag "[code,all,auto]")
("whether to highlight only the code parts (code) or " ++
"the file as a whole (all) or " ++
"decide by source file type (auto)")
]

htmlFlag :: Flag HtmlFlags
htmlFlag o = return $ o { htmlFlagEnabled = True }

htmlDirFlag :: FilePath -> Flag HtmlFlags
htmlDirFlag d o = return $ o { htmlFlagDir = d }

cssFlag :: FilePath -> Flag HtmlFlags
cssFlag f o = return $ o { htmlFlagCssFile = Just f }

highlightOccurrencesFlag :: Flag HtmlFlags
highlightOccurrencesFlag o = return $ o { htmlFlagHighlightOccurrences = True }

parseHtmlHighlightFlag :: MonadError String m => String -> m HtmlHighlight
parseHtmlHighlightFlag "code" = return HighlightCode
parseHtmlHighlightFlag "all" = return HighlightAll
parseHtmlHighlightFlag "auto" = return HighlightAuto
parseHtmlHighlightFlag opt = throwError $ concat ["Invalid option <", opt, ">, expected <all>, <auto> or <code>"]

htmlHighlightFlag :: String -> Flag HtmlFlags
htmlHighlightFlag opt o = do
flag <- parseHtmlHighlightFlag opt
return $ o { htmlFlagHighlight = flag }

runLogHtmlWithMonadDebug :: MonadDebug m => LogHtmlT m a -> m a
runLogHtmlWithMonadDebug = runLogHtmlWith $ reportS "html" 1

libToURL :: Map LibName String
libToURL = Map.fromList
[ ("agda-builtins", "https://agda.github.io/cubical")
, ("cubical-0.7", "https://agda.github.io/cubical")
, ("1lab", "https://1lab.dev")
]

preCompileHtml
:: HtmlFlags
-> TCM HtmlCompileEnv
preCompileHtml flags = do
moduleToSource <- useTC stModuleToSource
moduleToURL <- Map.traverseMaybeWithKey f moduleToSource
runLogHtmlWithMonadDebug $ do
logHtml $ unlines
[ "Warning: HTML is currently generated for ALL files which can be"
, "reached from the given module, including library files."
]
let opts = htmlOptsOfFlags flags
prepareCommonDestinationAssets opts
return $ HtmlCompileEnv opts moduleToURL
where
f :: TopLevelModuleName -> AbsolutePath -> TCM (Maybe String)
f m p = ((`Map.lookup` libToURL) <=< fmap _libName . listToMaybe) <$> getAgdaLibFiles p m

preModuleHtml
:: Applicative m
=> HtmlCompileEnv
-> IsMain
-> TopLevelModuleName
-> Maybe FilePath
-> m (Recompile HtmlModuleEnv HtmlModule)
preModuleHtml cenv _isMain modName _ifacePath = pure $ Recompile (HtmlModuleEnv cenv modName)

compileDefHtml
:: Applicative m
=> HtmlCompileEnv
-> HtmlModuleEnv
-> IsMain
-> Definition
-> m HtmlDef
compileDefHtml _env _menv _isMain _def = pure HtmlDef

postModuleHtml
:: (MonadIO m, MonadDebug m, ReadTCState m)
=> HtmlCompileEnv
-> HtmlModuleEnv
-> IsMain
-> TopLevelModuleName
-> [HtmlDef]
-> m HtmlModule
postModuleHtml env menv _isMain _modName _defs = do
let generatePage = defaultPageGen (htmlModuleToURL env) . htmlCompileEnvOpts . htmlModEnvCompileEnv $ menv
htmlSrc <- srcFileOfInterface (htmlModEnvName menv) <$> curIF
runLogHtmlWithMonadDebug $ generatePage htmlSrc
return HtmlModule

postCompileHtml
:: Applicative m
=> HtmlCompileEnv
-> IsMain
-> Map TopLevelModuleName HtmlModule
-> m ()
postCompileHtml _cenv _isMain _modulesByName = pure ()
Loading

0 comments on commit 502a11d

Please sign in to comment.