Plant Based UML Wiki
Published on 2016-11-04
So you want to do some UML? Perhaps integrate some diagrams into a wiki, or even better, make the diagrams modifiable/update-able using said wiki? Well, that is what I wanted and since I prefer to work with nix, pandoc and gitit these became the weapons of choice.
systemd
Furthermore, I wanted a systemd user service load my personal wiki and I also didn’t want to use rely on configuration.nix
, so I started from the following user service file:
0 ~ λ cat .config/systemd/user/gitit.service
[Unit]
Description=Personal Wiki
[Service]
Type=simple
WorkingDirectory=%h/.gitit
ExecStart=/home/edwtjo/.nix-profile/bin/gitit -f %h/.gitit/conf
Restart=no
[Install]
WantedBy=console.target
That was easy enough, albeit a bit ugly that we hard code $HOME into the service file but such is the requirements of systemd. I also created a shell alias to work with user services:
alias userctl="systemctl --user"
This of course is not really important, just a neat alias I think is worth repeating.
GHC
Next up we need to make the plugins loadable by gitit, it wants to load .dyn_o
files and there is some information regarding this here and here. Unfortunately I couldn’t figure out a way to change only the runtime default behaviour of GHC to compile dynamically using the haskell build expressions in nixpkgs. Also, using makewrapper and adding -dynamic-too
to NIX_GHC
turned out to be a fruitless exercise. At any rate; The manual way of compiling the plugins is to use, as noted in above links, -dynamic-too
cd ~/.gitit/plugins && ghc PlantUML.hs -dynamic-too
Gitit
Instead, I ended up inspecting the GHC API, and more specifically the GHC
and DynFlags
modules, with the intent of adding -dynamic-too
to the plugin loader of Gitit. Perhaps unsurprisingly enough, there is a GeneralFlag
constructor Opt_BuildDynamicToo
which can be used with getSessionDynFlags
and setSessionDynFlags
in order to update the plugin loader mechanism of gitit. Also, it seems we’re in luck! There is a function gopt_set
which takes a GeneralFlag
and a DynFlags
and flicks the switch in the generalFlags
IntSet
! We now have a gitit diff:
diff --git a/src/Network/Gitit/Plugins.hs b/src/Network/Gitit/Plugins.hs
index 0871d7a..ac85efe 100644
--- a/src/Network/Gitit/Plugins.hs
+++ b/src/Network/Gitit/Plugins.hs
@@ -29,6 +29,7 @@ import System.Log.Logger (logM, Priority(..))
#ifdef _PLUGINS
import Data.List (isInfixOf, isPrefixOf)
import GHC
+import DynFlags (gopt_set, GeneralFlag(..))
import GHC.Paths
import Unsafe.Coerce
@@ -37,7 +38,7 @@ loadPlugin pluginName = do
logM "gitit" WARNING ("Loading plugin '" ++ pluginName ++ "'...")
runGhc (Just libdir) $ do
dflags <- getSessionDynFlags
- setSessionDynFlags dflags
+ setSessionDynFlags (gopt_set dflags Opt_BuildDynamicToo)
defaultCleanupHandler dflags $ do
-- initDynFlags
unless ("Network.Gitit.Plugin." `isPrefixOf` pluginName)
And we save this to .nixpkgs/gitit-dyntoo.patch
.
PlantUML
module PlantUML (plugin) where
-- This plugin allows you to include a plantuml diagram
-- in a page like this:
--
-- ~~~ {.puml name="deployment"}
-- @startuml
-- cloud cloud1
-- cloud cloud2
-- cloud cloud3
-- cloud cloud4
-- cloud cloud5
-- cloud1 -0- cloud2
-- cloud1 -0)- cloud3
-- cloud1 -(0- cloud4
-- cloud1 -(0)- cloud5
-- @enduml
-- ~~~
--
-- The "dot" and "plantuml" executable must be in the path.
-- The generated png file will be saved in the static img directory.
import GHC.IO.Handle
import Network.Gitit.Interface
import System.Process (readProcessWithExitCode)
import System.Exit (ExitCode(ExitSuccess))
-- from the temporary package on HackageDB
import System.IO.Temp (withTempFile)
-- from the utf8-string package on HackageDB:
import Data.ByteString.Lazy.UTF8 (fromString)
import System.Environment (unsetEnv)
import System.FilePath ((</>))
import System.FilePath
plugin :: Plugin
plugin = mkPageTransformM transformBlock
transformBlock :: Block -> PluginM Block
transformBlock (CodeBlock (id, classes, namevals) contents) | "puml" `elem` classes = do
cfg <- askConfig
let filetype = "svg"
outdir = staticDir cfg </> "img"
liftIO $ withTempFile outdir "diag.puml" $ \infile inhandle -> do
unsetEnv "DISPLAY"
hPutStr inhandle contents
hClose inhandle
(ec, stdout, stderr) <- readProcessWithExitCode "plantuml"
[ "-t" ++ filetype
, infile
] ""
let outname = takeFileName $ infile -<.> filetype
if ec == ExitSuccess
then return $ Para [ Image (id,classes,namevals) [Str outname] ("/img" </> outname, "") ]
else error $ "plantuml returned an error status: " ++ stderr
transformBlock x = return x
Simply, we use a temporary puml
file in the static image directory, which gets deleted after the action finish. During the action we write the puml block contents to an input file and call out to plantuml, which in turn will write the output to the same place as the temporary file but with a new fileending. Also, we unset the DISPLAY
environment variable since splash screens are just the worst. We could be smarter here and adapt to the target format but this will do for now.
We can now save this plugin to .gitit/plugins/PlantUML.hs
and point to it in our gitit config like so:
...
plugins: /home/edwtjo/.gitit/plugins/PlantUML.hs
...
Nix
So now all we have left is to create a gitit
executable with plantuml added and all the dependencies of the plugin. We do this by creating a derivation in our .nixpkgs/config.nix
:
...
packageOverrides = self: with self: {
wiki = zoom: let
drv = let
env = with haskell.lib;
haskellPackages.ghcWithPackages
(self: with self;
[ (appendPatches (doJailbreak gitit) [ ./gitit-dyntoo.patch ])
utf8-string temporary tagsoup ]);
libDir = "${env}/lib/ghc-${env.version}";
in
stdenv.mkDerivation {
name = "personal-wiki";
phases = [ "installPhase" ];
installPhase =
let path = lib.makeBinPath
[ gitFull plantuml graphviz texlive.combined.scheme-medium ];
in ''
makeWrapper \
"${surf}/bin/surf -z ${toString zoom} 127.0.0.1:41934" \
$out/bin/wiki
makeWrapper ${env}/bin/gitit $out/bin/gitit \
--prefix PATH : ${path} \
--set "NIX_GHC" "${env}/bin/ghc" \
--set "NIX_GHC_LIBDIR" "${libDir}"
'';
};
in
lib.overrideDerivation drv (x : {buildInputs = x.buildInputs ++ [ makeWrapper ];});
wiki_1 = wiki 1;
wiki_2 = wiki 2;
};
...
You can ignore the zoom parameter, it is just something I use together with surf when I export presentations from gitit. Bear in mind that all packages are already in scope since we used self: with self;
and keep the derivation in the same file. So we don’t need to actually add anything to buildInputs
. In fact, we’re not really doing anything other than creating wrappers and we’re only using the installPhase from stdenv so we could’ve gotten away with a regular nix derivation. We do rely on the haskell library infrastructure to jailbreak gitit and apply the dyntoo patch from above.
We could, of course, also have added the gitit user service and gitit config as parametrized text files using Nix and have the wrapper create appropriate links in our home but this is left as an exercise to the reader.
Done
Nix install, start the user service, run the wiki command and create a new page containing:
~~~{.puml}
@startuml
Edward -> CoffeeMachine : switchOn
activate CoffeeMachine
CoffeeMachine -> Grinder : grindBeans
deactivate CoffeeMachine
activate Grinder
Grinder -> Grinder : fetchBeans
Grinder -> Grinder : runGrinder
Grinder --> CoffeeMachine : BeansInFilterCode
deactivate Grinder
activate CoffeeMachine #DarkSalmon
CoffeeMachine -> Boiler : heatWater
deactivate CoffeeMachine
activate Boiler
Boiler -> Boiler : awaitTemperature
Boiler --> CoffeeMachine : Temperature
deactivate Boiler
activate CoffeeMachine #red
CoffeeMachine -> CoffeeMachine : runBrewer
CoffeeMachine --> Edward : CoffeeCup
deactivate CoffeeMachine
@enduml
~~~
Which should render into: