packages feed

hydra-0.15.0: src/exec/transform-haskell-dsl-to-json/Main.hs

-- | Layer 1 transform: Haskell DSL -> JSON, scoped to a single package and
-- source set.
--
-- Usage:
--   transform-haskell-dsl-to-json --package <pkg> --source-set <main|test>
--                                 [--dist-json-root <dir>]
--
-- Reads the named package's manifest module (via Hydra.Sources.Ext, which
-- re-exports every per-package manifest), selects the requested source set,
-- and writes that set's JSON under <dist-json-root>/<pkg>/src/<set>/json/.
--
-- The full main-side universe (all packages) is used as the type-resolution
-- context, so cross-package type references resolve correctly. Only the
-- requested package's modules are actually written to disk.
--
-- Empty source sets (e.g. 'test' on a package with no test modules) exit 0
-- cleanly with a one-line note; callers can invoke unconditionally.
--
-- This is a Layer 1 transform per the sync system design in
-- feature_290_packaging-plan.md: stateless, idempotent, single
-- (package, source-set) pair per invocation. Composition is the orchestrator's
-- job, not this exec's.

module Main where

import Hydra.Generation (writeModulesJson, writeModulesJsonPackageSplit, writeDslJsonPackageSplit)
import Hydra.PackageRouting (defaultDistJsonRoot, namespaceToPackage)
import Hydra.Sources.Ext (
  mainModules, dslSourceModules,
  kernelModules, haskellModules, jsonModules, otherModules,
  hydraCoqModules, hydraJavaModules, hydraJavaScriptModules,
  hydraPythonModules, hydraScalaModules, hydraLispModules,
  hydraPgModules, hydraRdfModules, hydraWasmModules,
  hydraExtPackageModules,
  hydraExtDecodingModules, hydraExtEncodingModules)
import Hydra.Sources.Eval.Lib.All (evalLibModules)
import Hydra.Sources.Test.All (testModules)

import qualified Hydra.Kernel as Kernel
import qualified Hydra.Sources.Demos.GenPG.Transform as GenPGTransform

import Control.Monad (when)
import qualified Data.List as L
import qualified Data.Set as S
import System.Environment (getArgs)
import System.Exit (exitFailure, exitSuccess)
import qualified System.FilePath as FP


-- | Deduplicate a list of modules by namespace, keeping the first occurrence.
dedupByNamespace :: [Kernel.Module] -> [Kernel.Module]
dedupByNamespace = go S.empty
  where
    go _    []     = []
    go seen (m:ms)
      | ns `S.member` seen = go seen ms
      | otherwise          = m : go (S.insert ns seen) ms
      where ns = Kernel.moduleNamespace m


-- | All main-side modules across all packages, deduped. Used as the type-
-- resolution universe regardless of which package is being written.
--
-- Every package's DSL sources are included here so that per-package JSON
-- output can route modules to their owning package via namespaceToPackage.
fullMainUniverse :: [Kernel.Module]
fullMainUniverse = dedupByNamespace $ L.concat
  [ mainModules
  , evalLibModules
  , dslSourceModules
  , hydraCoqModules
  , hydraJavaModules
  , hydraJavaScriptModules
  , hydraPythonModules
  , hydraScalaModules
  , hydraLispModules
  , hydraPgModules
  , hydraRdfModules
  , hydraWasmModules
  , hydraExtPackageModules
  , hydraExtDecodingModules
  , hydraExtEncodingModules
  , [GenPGTransform.module_]
  ]


data Options = Options
  { optPackage      :: Maybe String
  , optSourceSet    :: String
  , optDistJsonRoot :: FilePath
  }

defaultOptions :: Options
defaultOptions = Options
  { optPackage      = Nothing
  , optSourceSet    = "main"
  , optDistJsonRoot = defaultDistJsonRoot
  }


parseArgs :: [String] -> Either String Options
parseArgs = go defaultOptions
  where
    go opts [] = case optPackage opts of
      Nothing -> Right opts  -- no --package means "all packages" (batch mode)
      Just _  -> Right opts
    go opts ("--package" : p : rest) = go (opts { optPackage = Just p }) rest
    go opts ("--all" : rest) = go (opts { optPackage = Nothing }) rest
    go opts ("--source-set" : s : rest)
      | s == "main" || s == "test" = go (opts { optSourceSet = s }) rest
      | otherwise                  = Left $ "--source-set must be 'main' or 'test', got: " ++ s
    go opts ("--dist-json-root" : d : rest) = go (opts { optDistJsonRoot = d }) rest
    go _    (arg : _) = Left $ "Unknown argument: " ++ arg


usage :: String
usage = unlines
  [ "Usage: transform-haskell-dsl-to-json [--package <pkg>] [OPTIONS]"
  , ""
  , "Options:"
  , "  --package <pkg>          Package to transform. If omitted (or --all"
  , "                           is passed), every package is transformed in a"
  , "                           single Haskell-universe load."
  , "                           Packages: hydra-kernel, hydra-haskell,"
  , "                           hydra-java, hydra-python, hydra-scala,"
  , "                           hydra-lisp, hydra-coq, hydra-javascript,"
  , "                           hydra-pg, hydra-rdf, hydra-ext, hydra-wasm."
  , "  --all                    Batch mode: transform every package."
  , "  --source-set <main|test> Source set to transform (default: main)."
  , "                           'test' is only non-empty for hydra-kernel today."
  , "  --dist-json-root <dir>   Output root (default: ../../dist/json)."
  ]


-- | Every supported package (ordering matches hydra.json).
allPackages :: [String]
allPackages =
  [ "hydra-kernel"
  , "hydra-haskell"
  , "hydra-java"
  , "hydra-python"
  , "hydra-scala"
  , "hydra-lisp"
  , "hydra-coq"
  , "hydra-javascript"
  , "hydra-wasm"
  , "hydra-pg"
  , "hydra-rdf"
  , "hydra-ext"
  ]


-- | Return the test modules owned by the given package. Today, only the
-- kernel has any test modules; other packages return [].
packageTestModules :: String -> [Kernel.Module]
packageTestModules "hydra-kernel" = testModules
packageTestModules _              = []


-- | Return the type-defining modules whose DSL wrappers should be generated
-- for this package. Only kernel and hydra-haskell have DSL wrappers today;
-- other packages return [] and skip DSL generation.
--
-- Mirrors update-json-main's dslInputMods composition: the kernel DSL pass
-- covers kernel + json + other modules; the haskell DSL pass covers haskell
-- coder modules. Other packages don't produce DSL wrappers.
packageDslInputModules :: String -> [Kernel.Module]
packageDslInputModules "hydra-kernel"  = kernelModules ++ jsonModules ++ otherModules
packageDslInputModules "hydra-haskell" = haskellModules
packageDslInputModules _               = []


main :: IO ()
main = do
  args <- getArgs
  opts <- case parseArgs args of
    Left err -> do
      putStrLn $ "Error: " ++ err
      putStrLn ""
      putStrLn usage
      exitFailure
    Right o -> return o

  let srcSet    = optSourceSet opts
      distRoot  = optDistJsonRoot opts

  case optPackage opts of
    Just pkg -> runSinglePackage pkg srcSet distRoot
    Nothing  -> runAllPackages      srcSet distRoot

  putStrLn "=== Done. ==="

-- | Transform a single package. Historical behaviour, kept for scoped
-- callers that invoke one package at a time.
runSinglePackage :: String -> String -> FilePath -> IO ()
runSinglePackage pkg srcSet distRoot = do
  let outDir = distRoot FP.</> pkg FP.</> "src" FP.</> srcSet FP.</> "json"
  putStrLn $ "=== Transform Haskell DSL -> JSON: " ++ pkg ++ " (" ++ srcSet ++ ") ==="

  let (mods, universe) = case srcSet of
        "main" ->
          let owned = filter (\m -> namespaceToPackage (Kernel.moduleNamespace m) == pkg)
                             fullMainUniverse
          in (owned, fullMainUniverse)
        "test" ->
          let ts       = packageTestModules pkg
              testUniv = fullMainUniverse ++ ts
          in (ts, testUniv)
        _ -> ([], fullMainUniverse)

  when (null mods) $ do
    putStrLn $ "  (no modules in " ++ pkg ++ "/" ++ srcSet ++ "; skipping)"
    exitSuccess

  putStrLn $ "  Writing " ++ show (length mods) ++ " modules -> " ++ outDir

  case srcSet of
    "main" -> do
      writeModulesJsonPackageSplit True distRoot universe mods
      let dslInputs = packageDslInputModules pkg
      case dslInputs of
        [] -> return ()
        _  -> do
          putStrLn ""
          putStrLn $ "  Generating DSL wrapper modules for "
            ++ show (length dslInputs) ++ " type modules..."
          writeDslJsonPackageSplit distRoot universe dslInputs
    "test" -> writeModulesJson True outDir universe mods
    _      -> return ()

-- | Batch mode: transform every package with one Haskell universe load.
-- writeModulesJsonPackageSplit routes modules to their owning package's
-- per-package JSON tree based on namespaceToPackage, so passing the whole
-- universe produces the same on-disk layout as per-package calls — but
-- we only pay the Haskell startup + DSL-compile cost once.
runAllPackages :: String -> FilePath -> IO ()
runAllPackages srcSet distRoot = do
  putStrLn $ "=== Transform Haskell DSL -> JSON: all packages (" ++ srcSet ++ ") ==="

  case srcSet of
    "main" -> do
      putStrLn $ "  Writing " ++ show (length fullMainUniverse)
        ++ " main modules (routed per-package)..."
      writeModulesJsonPackageSplit True distRoot fullMainUniverse fullMainUniverse
      -- DSL wrappers: generate for every package that has type-defining
      -- inputs. The routing is implicit — wrapper namespaces like
      -- hydra.dsl.<pkg>.* resolve to their owning package via
      -- packagePrefixes in PackageRouting.
      let allDslInputs = concatMap packageDslInputModules allPackages
      when (not (null allDslInputs)) $ do
        putStrLn ""
        putStrLn $ "  Generating DSL wrapper modules for "
          ++ show (length allDslInputs) ++ " type modules..."
        writeDslJsonPackageSplit distRoot fullMainUniverse allDslInputs
    "test" -> do
      let allTestMods = concatMap packageTestModules allPackages
          testUniverse = fullMainUniverse ++ allTestMods
      when (null allTestMods) $ do
        putStrLn "  (no test modules in any package; skipping)"
        exitSuccess
      -- Today only hydra-kernel has tests, and its tests live at a flat
      -- hydra-kernel/src/test/json/ location. writeModulesJson is the
      -- direct (non-routing) writer.
      let kernelTestDir = distRoot FP.</> "hydra-kernel" FP.</> "src"
            FP.</> "test" FP.</> "json"
      putStrLn $ "  Writing " ++ show (length allTestMods)
        ++ " test modules -> " ++ kernelTestDir
      writeModulesJson True kernelTestDir testUniverse allTestMods
    _ -> return ()