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:


Comments