From bd331dc379a1f3e6967d7e98e032a860ed30fccb Mon Sep 17 00:00:00 2001 From: sternenseemann Date: Sun, 14 Apr 2024 15:05:58 +0200 Subject: [PATCH] ghc-settings-edit: init at 0.1.0 --- .../haskell-modules/non-hackage-packages.nix | 2 + .../haskell/ghc-settings-edit/default.nix | 33 ++++++ .../ghc-settings-edit/ghc-settings-edit.lhs | 109 ++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 pkgs/development/tools/haskell/ghc-settings-edit/default.nix create mode 100644 pkgs/development/tools/haskell/ghc-settings-edit/ghc-settings-edit.lhs diff --git a/pkgs/development/haskell-modules/non-hackage-packages.nix b/pkgs/development/haskell-modules/non-hackage-packages.nix index bb995435fc60..c5cfbcec0313 100644 --- a/pkgs/development/haskell-modules/non-hackage-packages.nix +++ b/pkgs/development/haskell-modules/non-hackage-packages.nix @@ -17,6 +17,8 @@ self: super: { # from the latest master instead of the current version on Hackage. cabal2nix-unstable = self.callPackage ./cabal2nix-unstable.nix { }; + ghc-settings-edit = self.callPackage ../tools/haskell/ghc-settings-edit { }; + # https://github.com/channable/vaultenv/issues/1 vaultenv = self.callPackage ../tools/haskell/vaultenv { }; diff --git a/pkgs/development/tools/haskell/ghc-settings-edit/default.nix b/pkgs/development/tools/haskell/ghc-settings-edit/default.nix new file mode 100644 index 000000000000..f1f08b620588 --- /dev/null +++ b/pkgs/development/tools/haskell/ghc-settings-edit/default.nix @@ -0,0 +1,33 @@ +{ + stdenv, + ghc, + lib, +}: + +stdenv.mkDerivation { + pname = "ghc-settings-edit"; + version = "0.1.0"; + # See the source code for an explanation + src = ./ghc-settings-edit.lhs; + dontUnpack = true; + dontBuild = true; + + nativeBuildInputs = [ ghc ]; + installPhase = '' + runHook preInstall + + mkdir -p "$out/bin" + ${ghc.targetPrefix}ghc --make "$src" -o "$out/bin/ghc-settings-edit" + + runHook postInstall + ''; + + meta = { + license = [ + lib.licenses.mit + lib.licenses.bsd3 + ]; + platforms = lib.platforms.all; + description = "Tool for editing GHC's settings file"; + }; +} diff --git a/pkgs/development/tools/haskell/ghc-settings-edit/ghc-settings-edit.lhs b/pkgs/development/tools/haskell/ghc-settings-edit/ghc-settings-edit.lhs new file mode 100644 index 000000000000..3b37bafe0d62 --- /dev/null +++ b/pkgs/development/tools/haskell/ghc-settings-edit/ghc-settings-edit.lhs @@ -0,0 +1,109 @@ +ghc-settings-edit is a small tool for changing certain fields in the settings +file that is part of every GHC installation (usually located at +lib/ghc-$version/lib/settings or lib/ghc-$version/settings). This is sometimes +necessary because GHC's build process leaks the tools used at build time into +the final settings file. This is fine, as long as the build and host platform +of the GHC build is the same since it will be possible to execute the tools +used at build time at run time. In case we are cross compiling GHC itself, +the settings file needs to be changed so that the correct tools are used in the +final installation. The GHC build system itself doesn't allow for this due to +its somewhat peculiar bootstrapping mechanism. + +This tool was originally written by sternenseemann and is licensed under the MIT +license (as is nixpkgs) as well as the BSD 3 Clause license since it incorporates +some code from GHC. It is primarily intended for use in nixpkgs, so it should be +considered unstable: No guarantees about the stability of its command line +interface are made at this time. + +> -- SPDX-License-Identifier: MIT AND BSD-3-Clause +> {-# LANGUAGE LambdaCase #-} +> module Main where + +ghc-settings-edit requires no additional dependencies to the ones already +required to bootstrap GHC. This means that it only depends on GHC and core +libraries shipped with the compiler (base and containers). This property should +be preserved going forward as to not needlessly complicate bootstrapping GHC +in nixpkgs. Additionally, a wide range of library versions and thus GHC versions +should be supported (via CPP if necessary). + +> import Control.Monad (foldM) +> import qualified Data.Map.Lazy as Map +> import System.Environment (getArgs, getProgName) +> import Text.Read (readEither) + +Note that the containers dependency is needed to represent the contents of the +settings file. In theory, [(String, String)] (think lookup) would suffice, but +base doesn't provide any facilities for updating such lists. To avoid needlessly +reinventing the wheel here, we depend on an extra core library. + +> type SettingsMap = Map.Map String String + +ghc-settings-edit accepts the following arguments: + +- The path to the settings file which is edited in place. +- For every field in the settings file to be updated, two arguments need to be + passed: the name of the field and its new value. Any number of these pairs + may be provided. If a field is missing from the given settings file, + it won't be added (see also below). + +> usage :: String -> String +> usage name = "Usage: " ++ name ++ " FILE [KEY NEWVAL [KEY2 NEWVAL2 ...]]" + +The arguments and the contents of the settings file are fed into the performEdits +function which implements the main logic of ghc-settings-edit (except IO). + +> performEdits :: [String] -> String -> Either String String +> performEdits editArgs settingsString = do + +First, the settings file is parsed and read into the SettingsMap structure. For +parsing, we can simply rely read, as GHC uses the familiar Read/Show format +(plus some formatting) for storing its settings. This is the main reason +ghc-settings-edit is written in Haskell: We don't need to roll our own parser. + +> settingsMap <- Map.fromList <$> readEither settingsString + +We also need to parse the remaining command line arguments (after the path) +which means splitting them into pairs of arguments describing the individual +edits. We use the chunkList utility function from GHC for this which is vendored +below. Since it doesn't guarantee that all sublists have the exact length given, +we'll have to check the length of the returned “pairs” later. + +> let edits = chunkList 2 editArgs + +Since each edit is a transformation of the SettingsMap, we use a fold to go +through the edits. The Either monad allows us to bail out if one is malformed. +The use of Map.adjust ensures that fields that aren't present in the original +settings file aren't added since the corresponding GHC installation wouldn't +understand them. Note that this is done silently which may be suboptimal: +It could be better to fail. + +> show . Map.toList <$> foldM applyEdit settingsMap edits +> where +> applyEdit :: SettingsMap -> [String] -> Either String SettingsMap +> applyEdit m [key, newValue] = Right $ Map.adjust (const newValue) key m +> applyEdit _ _ = Left "Uneven number of edit arguments provided" + +main just wraps performEdits and takes care of reading from and writing to the +given file. + +> main :: IO () +> main = +> getArgs >>= \case +> (settingsFile:edits) -> do +> orig <- readFile settingsFile +> case performEdits edits orig of +> Right edited -> writeFile settingsFile edited +> Left errorMsg -> error errorMsg +> _ -> do +> name <- getProgName +> error $ usage name + +As mentioned, chunkList is taken from GHC, specifically GHC.Utils.Misc of GHC +verson 9.8.2. We don't depend on the ghc library directly (which would be +possible in theory) since there are no stability guarantees or deprecation +windows for the ghc's public library. + +> -- | Split a list into chunks of /n/ elements +> chunkList :: Int -> [a] -> [[a]] +> chunkList _ [] = [] +> chunkList n xs = as : chunkList n bs where (as,bs) = splitAt n xs