content: -- Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
  -- SPDX-License-Identifier: Apache-2.0
  
  {-# LANGUAGE TemplateHaskell     #-}
  {-# LANGUAGE ApplicativeDo       #-}
  {-# LANGUAGE CPP #-}
  {-# LANGUAGE MultiWayIf #-}
  {-# LANGUAGE TypeFamilies #-}
  {-# LANGUAGE RankNTypes #-}
  {-# LANGUAGE DeriveAnyClass #-}
  
  -- | Main entry-point of the Daml compiler
  module DA.Cli.Damlc (main, Command (..), MultiPackageManifestEntry (..), fullParseArgs) where
  
  import qualified "zip-archive" Codec.Archive.Zip as ZipArchive
  import Control.Exception (bracket, catch, displayException, throwIO, handle, throw)
  import Control.Exception.Safe (catchIO)
  import Control.Monad (forM, forM_, unless, void, when)
  import Control.Monad.IO.Class (liftIO)
  import Control.Monad.Extra (allM, mapMaybeM, whenM, whenJust)
  import Control.Monad.Trans.Cont (ContT (..), evalContT)
  import qualified Crypto.Hash as Hash
  import DA.Bazel.Runfiles (setRunfilesEnv)
  import qualified DA.Cli.Args as ParseArgs
  import DA.Cli.Options (Debug(..),
                         EnableMultiPackage(..),
                         GenerateMultiPackageManifestOutput (..),
                         InitPkgDb(..),
                         MultiPackageBuildAll(..),
                         MultiPackageCleanAll(..),
                         MultiPackageLocation(..),
                         MultiPackageNoCache(..),
                         PackageLocationOpts(..),
                         Style(..),
                         Telemetry(..),
                         cliOptDetailLevel,
                         cliOptLogLevel,
                         debugOpt,
                         disabledDlintUsageParser,
                         enabledDlintUsageParser,
                         enableMultiPackageOpt,
                         enableScriptServiceOpt,
                         generateMultiPackageManifestOutputOpt,
                         incrementalBuildOpt,
                         initPkgDbOpt,
                         inputDarOpt,
                         inputFileOpt,
                         inputFileOptWithExt,
                         multiPackageBuildAllOpt,
                         multiPackageCleanAllOpt,
                         multiPackageLocationOpt,
                         multiPackageNoCacheOpt,
                         optionalDlintUsageParser,
                         optionalOutputFileOpt,
                         optionsParser,
                         optPackageName,
                         outputFileOpt,
                         packageLocationOpts,
                         render,
                         studioAutorunAllScriptsOpt,
                         studioReplaceOpt,
                         telemetryOpt)
  import DA.Cli.Damlc.BuildInfo (buildInfo)
  import DA.Cli.Damlc.Command.MultiIde (runMultiIde)
  import DA.Cli.Damlc.Command.UpgradeCheck (runUpgradeCheck)
  import qualified DA.Daml.Dar.Reader as InspectDar
  import qualified DA.Cli.Damlc.Command.Damldoc as Damldoc
  import DA.Cli.Damlc.Packaging (setupPackageDb, setupPackageDbFromPackageConfig, mbErr)
  import DA.Cli.Damlc.Test (CoveragePaths(..),
                            LoadCoverageOnly(..),
                            RunAllOption(..),
                            ShowCoverage(..),
                            TableOutputPath(..),
                            TransactionsOutputPath(..),
                            UseColor(..),
                            execTest,
                            getRunAllTests,
                            loadAggregatePrintResults,
                            CoverageFilter(..))
  import DA.Daml.Compiler.Dar (FromDalf(..),
                               buildDar,
                               createDarFile,
                               damlFilesInDir,
                               getDamlRootFiles,
                               writeIfacesAndHie)
  import DA.Daml.Compiler.Output (diagnosticsLogger, writeOutput, writeOutputBSL)
  import DA.Daml.Project.Types
      ( UnresolvedReleaseVersion(..),
        unresolvedReleaseVersionToString,
        parseUnresolvedVersion,
        isHeadVersion,
        ConfigError(..),
        PackagePath(..),
        PackageConfig,
        unsafeResolveReleaseVersion)
  import DA.Daml.Assistant.Version (resolveReleaseVersionUnsafe)
  import DA.Daml.Assistant.Util (wrapErr)
  import DA.Daml.Compiler.DocTest (docTest)
  import DA.Daml.Desugar (desugar)
  import DA.Daml.Helper.Studio (runDamlStudio)
  import DA.Daml.LF.ScriptServiceClient (readScriptServiceConfig, withScriptService')
  import DA.Daml.Compiler.Validate (validateDar)
  import qualified DA.Daml.LF.Ast as LF
  import qualified DA.Daml.LF.Proto3.Archive as Archive
  import DA.Daml.LF.Reader (mainDalf,
                            mainDalfPath,
                            readDalfManifest,
                            readDalfs)
  import qualified DA.Daml.LF.Reader as Reader
  import DA.Daml.LanguageServer (runLanguageServer)
  import DA.Daml.Options (toCompileOpts)
  import DA.Daml.Options.Types (EnableScriptService(..),
                                Haddock(..),
                                IncrementalBuild (..),
                                Options(Options),
                                SkipScriptValidation(..),
                                StudioAutorunAllScripts,
                                UpgradeInfo(..),
                                damlArtifactDir,
                                distDir,
                                getLogger,
                                ifaceDir,
                                optDamlLfVersion,
                                optEnableOfInterestRule,
                                optHaddock,
                                optHideUnitId,
                                optIfaceDir,
                                optImportPath,
                                optIncrementalBuild,
                                optMbPackageConfigPath,
                                optMbPackageName,
                                optMbPackageVersion,
                                optPackageDbs,
                                optPackageImports,
                                optResolutionData,
                                optScriptService,
                                optSkipScriptValidation,
                                optThreads,
                                optUpgradeInfo,
                                optTypecheckerWarningFlags,
                                pkgNameVersion,
                                packageDatabasePath)
  import DA.Daml.Package.Config (MultiPackageConfigFields(..),
                                 PackageConfigFields(..),
                                 checkPkgConfig,
                                 findMultiPackageConfig,
                                 withPackageConfig,
                                 withMultiPackageConfig)
  import DA.Daml.Resolution.Config (ValidPackageResolution (..), ResolutionError (..), DPMUnsupportedError (..), findPackageResolutionData, getResolutionData)
  import DA.Daml.Project.Config (queryPackageConfig, queryPackageConfigRequired, readPackageConfig)
  import DA.Daml.Project.Consts (PackageLocationCheck(..),
                                 damlCacheEnvVar,
                                 damlPathEnvVar,
                                 getPackagePath,
                                 getSdkVersion,
                                 multiPackageConfigName,
                                 packageConfigName,
                                 withExpectPackageRoot,
                                 withPackageRoot,
                                 damlAssistantIsSet,
                                 projectPathEnvVar,
                                 packagePathEnvVar)
  import DA.Daml.Assistant.Env (getDamlEnv, getDamlPath, envUseCache)
  import DA.Daml.Assistant.Types (LookForPackagePath(..))
  import qualified DA.Pretty
  import qualified DA.Service.Logger as Logger
  import qualified DA.Service.Logger.Impl.GCP as Logger.GCP
  import qualified DA.Service.Logger.Impl.IO as Logger.IO
  import DA.Signals (installSignalHandlers)
  import qualified Com.Digitalasset.Daml.Lf.Archive.DamlLf as PLF
  import Data.Aeson (FromJSON, ToJSON)
  import qualified Data.Aeson.Encode.Pretty as Aeson.Pretty
  import qualified Data.Aeson.Text as Aeson
  import Data.Bifunctor (bimap, second)
  import qualified Data.ByteString as B
  import qualified Data.ByteString.Lazy as BSL
  import qualified Data.ByteString.Lazy.Char8 as BSLC
  import qualified Data.ByteString.UTF8 as BSUTF8
  import Data.Either (partitionEithers)
  import Data.FileEmbed (embedFile)
  import qualified Data.HashSet as HashSet
  import Data.List (isPrefixOf, isInfixOf)
  import Data.List.Extra (elemIndices, nubOrd, nubSort)
  import qualified Data.Map.Strict as Map
  import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe)
  import qualified Data.Text.Extended as T
  import Data.Text.Encoding (encodeUtf8)
  import qualified Data.Text.Lazy.IO as TL
  import qualified Data.Text.IO as T
  import Data.Traversable (for)
  import Data.Typeable (Typeable)
  import qualified Data.Yaml as Y
  import Development.IDE.Core.API (getDalf, makeVFSHandle, runActionSync, setFilesOfInterest)
  import Development.IDE.Core.Debouncer (newAsyncDebouncer, noopDebouncer)
  import Development.IDE.Core.IdeState.Daml (getDamlIdeState,
                                             enabledPlugins,
                                             withDamlIdeState,
                                             toIdeLogger,
                                             scriptServiceJarFromOptions)
  import Development.IDE.Core.Rules (transitiveModuleDeps)
  import Development.IDE.Core.Rules.Daml (getDlintIdeas, getSpanInfo)
  import Development.IDE.Core.Shake (Config(..),
                                     IdeResult,
                                     NotificationHandler(..),
                                     ShakeLspEnv(..),
                                     actionLogger,
                                     defineEarlyCutoffWithDefaultRunChanged,
                                     getDiagnostics,
                                     use,
                                     use_,
                                     uses,
                                     uses_)
  import Development.IDE.GHC.Util (hscEnv, moduleImportPath)
  import Development.IDE.Types.Location (NormalizedFilePath, fromNormalizedFilePath, toNormalizedFilePath')
  import "ghc-lib-parser" DynFlags (DumpFlag(..),
                                    ModRenaming(..),
                                    PackageArg(..),
                                    PackageFlag(..))
  import GHC.Conc (getNumProcessors)
  import "ghc-lib-parser" Module (unitIdString, stringToUnitId)
  import qualified Network.Socket as NS
  import Options.Applicative.Extended (flagYesNoAuto, strOptionOnce)
  import Options.Applicative ((<|>),
                              CommandFields,
                              Mod,
                              Parser,
                              ParserInfo(..),
                              ParserResult(..),
                              command,
                              defaultPrefs,
                              execParserPure,
                              flag,
                              flag',
                              forwardOptions,
                              fullDesc,
                              handleParseResult,
                              headerDoc,
                              help,
                              helper,
                              info,
                              internal,
                              long,
                              many,
                              metavar,
                              optional,
                              prefs,
                              progDesc,
                              renderFailure,
                              strArgument,
                              subparser,
                              switch,
                              value)
  import qualified Options.Applicative (option, strOption)
  import qualified Options.Applicative.Types as Options.Applicative (readerAsk)
  import qualified Proto3.Suite as PS
  import qualified Proto3.Suite.JSONPB as Proto.JSONPB
  import System.Directory.Extra
  import System.Environment
  import System.Exit
  import System.FilePath
  import System.IO.Extra
  import System.Process (CreateProcess(..),
                        proc,
                        withCreateProcess,
                        waitForProcess,
                        )
  import qualified Text.PrettyPrint.ANSI.Leijen as PP
  import Development.IDE.Core.RuleTypes
  import "ghc-lib-parser" ErrUtils
  -- For dumps
  import "ghc-lib" GHC
  import "ghc-lib-parser" HsDumpAst
  import "ghc-lib" HscStats
  import "ghc-lib-parser" HscTypes
  import qualified "ghc-lib-parser" Outputable as GHC
  import qualified SdkVersion.Class
  import Text.Regex.TDFA
  
  import qualified Development.IDE.Core.Service as IDE
  import Control.DeepSeq
  import Data.Binary
  import Data.Hashable
  import GHC.Generics (Generic)
  import Development.Shake (Rules, RuleResult)
  import Development.Shake.Rule (RunChanged (ChangedRecomputeDiff, ChangedRecomputeSame))
  import qualified Development.IDE.Types.Logger as IDELogger
  
  --------------------------------------------------------------------------------
  -- Commands
  --------------------------------------------------------------------------------
  
  data CommandName =
      Build
    | Clean
    | Compile
    | DamlDoc
    | DebugIdeSpanInfo
    | Desugar
    | DocTest
    | Ide
    | Init
    | Inspect
    | InspectDar
    | ValidateDar
    | License
    | Lint
    | Test
    | GenerateMultiPackageManifest
    | MultiIde
    | UpgradeCheck
    | Studio
    deriving (Ord, Show, Eq)
  data Command = Command CommandName (Maybe PackageLocationOpts) (IO ())
  
  cmdMultiIde :: SdkVersion.Class.SdkVersioned => Int -> Mod CommandFields Command
  cmdMultiIde _numProcessors =
      command "multi-ide" $ info (helper <*> cmd) $
         progDesc
          "Start the Daml Multi-IDE language server on standard input/output."
      <> fullDesc
      <> forwardOptions
    where
      cmd = fmap (Command MultiIde Nothing) $ runMultiIde
          <$> cliOptLogLevel
          <*> optional (strOptionOnce $ long "ide-identifier" <> help "Identifier string for this IDE")
          <*> many (strArgument mempty)
  
  cmdUpgradeCheck :: SdkVersion.Class.SdkVersioned => Int -> Mod CommandFields Command
  cmdUpgradeCheck _numProcessors =
      command "upgrade-check" $ info (helper <*> cmd) $
         progDesc
          "Check upgrades for multiple DARs"
      <> fullDesc
      <> forwardOptions
    where
      cmd = fmap (Command UpgradeCheck Nothing) $ runUpgradeCheck
          <$> many (strArgument $ help "Path to DAR")
  
  cmdIde :: SdkVersion.Class.SdkVersioned => Int -> Mod CommandFields Command
  cmdIde numProcessors =
      command "ide" $ info (helper <*> cmd) $
         progDesc
          "Start the Daml language server on standard input/output."
      <> fullDesc
    where
      cmd = execIde
          <$> telemetryOpt
          <*> debugOpt
          <*> enableScriptServiceOpt
          <*> studioAutorunAllScriptsOpt
          <*> optionsParser
                numProcessors
                (EnableScriptService True)
                (pure Nothing)
                (optionalDlintUsageParser True)
  
  cmdLicense :: Mod CommandFields Command
  cmdLicense =
      command "license" $ info (helper <*> pure execLicense) $
         progDesc
          "License information for open-source projects included in Daml."
      <> fullDesc
  
  cmdCompile :: SdkVersion.Class.SdkVersioned => Int -> Mod CommandFields Command
  cmdCompile numProcessors =
      command "compile" $ info (helper <*> cmd) $
          progDesc "Compile the Daml program into a Core/Daml-LF archive."
      <> fullDesc
    where
      cmd = execCompile
          <$> inputFileOpt
          <*> outputFileOpt
          <*> optionsParser
                numProcessors
                (EnableScriptService False)
                optPackageName
                disabledDlintUsageParser
          <*> optWriteIface
          <*> optional (strOptionOnce $ long "iface-dir" <> metavar "IFACE_DIR" <> help "Directory for interface files")
  
      optWriteIface =
          fmap WriteInterface $
          switch $
          help "Produce interface files. This is used for building the package db for daml-prim and daml-stdib" <>
          long "write-iface"
  
  cmdDesugar :: SdkVersion.Class.SdkVersioned => Int -> Mod CommandFields Command
  cmdDesugar numProcessors =
    command "desugar" $ info (helper <*> cmd) $
        progDesc "Show the desugared Daml program"
      <> fullDesc
    where
      cmd = execDesugar
        <$> inputFileOpt
        <*> outputFileOpt
        <*> optionsParser
              numProcessors
              (EnableScriptService False)
              optPackageName
              disabledDlintUsageParser
  
  cmdDebugIdeSpanInfo :: SdkVersion.Class.SdkVersioned => Int -> Mod CommandFields Command
  cmdDebugIdeSpanInfo numProcessors =
    command "debug-ide-span-info" $ info (helper <*> cmd) $
        progDesc "Show the IDE span infos for the Daml program"
      <> fullDesc
    where
      cmd = execDebugIdeSpanInfo
        <$> inputFileOpt
        <*> outputFileOpt
        <*> optionsParser
              numProcessors
              (EnableScriptService False)
              optPackageName
              disabledDlintUsageParser
  
  cmdLint :: SdkVersion.Class.SdkVersioned => Int -> Mod CommandFields Command
  cmdLint numProcessors =
      command "lint" $ info (helper <*> cmd) $
          progDesc "Lint the Daml program."
      <> fullDesc
    where
      cmd = execLint
          <$> many inputFileOpt
          <*> optionsParser
                numProcessors
                (EnableScriptService False)
                optPackageName
                enabledDlintUsageParser
  
  cmdTest :: SdkVersion.Class.SdkVersioned => Int -> Mod CommandFields Command
  cmdTest numProcessors =
      command "test" $ info (helper <*> cmd) $
         progDesc progDoc
      <> fullDesc
    where
      progDoc = unlines
        [ "Test the current Daml package or the given files by running all test declarations."
        , "Must be in Daml package if --files is not set."
        ]
      cmd = runTestsInPackageOrFiles
        <$> packageLocationOpts "daml test"
        <*> filesOpt
        <*> fmap RunAllOption runAllOption
        <*> fmap LoadCoverageOnly loadCoverageOnly
        <*> fmap ShowCoverage showCoverageOpt
        <*> fmap UseColor colorOutput
        <*> junitOutput
        <*> optionsParser
              numProcessors
              (EnableScriptService True)
              optPackageName
              disabledDlintUsageParser
        <*> initPkgDbOpt
        <*> fmap TableOutputPath tableOutputPathOpt
        <*> fmap TransactionsOutputPath transactionsOutputPathOpt
        <*> coveragePathsOpt
        <*> many coverageFilterSkipOpt
      filesOpt = optional (flag' () (long "files" <> help filesDoc) *> many inputFileOpt)
      filesDoc = "Only run test declarations in the specified files."
      junitOutput = optional $ strOptionOnce $ long "junit" <> metavar "FILENAME" <> help "Filename of JUnit output file"
      colorOutput = switch $ long "color" <> help "Colored test results"
      showCoverageOpt = switch $ long "show-coverage" <> help "Show detailed test coverage"
      runAllOption = switch $ long "all" <> help "Run tests in current package as well as dependencies"
      tableOutputPathOpt = optional $ strOptionOnce $ long "table-output" <> help "Filename to which table should be output"
      transactionsOutputPathOpt = optional $ strOptionOnce $ long "transactions-output" <> help "Filename to which the transaction list should be output"
      coveragePathsOpt =
        let loadCoveragePaths = many $ Options.Applicative.strOption $ long "load-coverage" <> help "File to read prior coverage results from. Can be specified more than once."
            saveCoveragePath = optional $ strOptionOnce $ long "save-coverage" <> help "File to write final aggregated coverage results to."
        in
        CoveragePaths <$> loadCoveragePaths <*> saveCoveragePath
      loadCoverageOnly = switch $ long "load-coverage-only" <> help "Don't run any tests - only load coverage results from files and write the aggregate to a single file."
      coverageFilterSkipOpt =
        Options.Applicative.option (Options.Applicative.readerAsk >>= fmap CoverageFilter . makeRegexM) $
          long "coverage-ignore-choice" <> help "Remove choices matching a regex from the coverage report. The full name of a local choice takes the format '<module>:<template name>:<choice name>', preceded by '<package id>:' for nonlocal packages."
  
  runTestsInPackageOrFiles ::
         SdkVersion.Class.SdkVersioned
      => PackageLocationOpts
      -> Maybe [FilePath]
      -> RunAllOption
      -> LoadCoverageOnly
      -> ShowCoverage
      -> UseColor
      -> Maybe FilePath
      -> Options
      -> InitPkgDb
      -> TableOutputPath
      -> TransactionsOutputPath
      -> CoveragePaths
      -> [CoverageFilter]
      -> Command
  runTestsInPackageOrFiles packageLocationOpts mbInFiles allTests (LoadCoverageOnly True) coverage _ _ _ _ _ _ coveragePaths coverageFilters = Command Test (Just packageLocationOpts) effect
    where effect = do
            when (getRunAllTests allTests) $ do
              hPutStrLn stderr "Cannot specify --all and --load-coverage-only at the same time."
              exitFailure
            case mbInFiles of
              Just _ -> do
                hPutStrLn stderr "Cannot specify --all and --load-coverage-only at the same time."
                exitFailure
              Nothing -> do
                loadAggregatePrintResults coveragePaths coverageFilters coverage Nothing
  runTestsInPackageOrFiles packageLocationOpts Nothing allTests _ coverage color mbJUnitOutput cliOptions initPkgDb tableOutputPath transactionsOutputPath coveragePaths coverageFilters = Command Test (Just packageLocationOpts) effect
    where effect = withExpectPackageRoot (packageRoot packageLocationOpts) "daml test" $ \pPath relativize -> do
            cliOptions <- addResolutionData cliOptions
            cliOptions <- pure $ cliOptions { optMbPackageConfigPath = Just $ PackagePath pPath }
            installDepsAndInitPackageDb cliOptions initPkgDb
            mbJUnitOutput <- traverse relativize mbJUnitOutput
            withPackageConfig (PackagePath pPath) $ \pkgConfig@PackageConfigFields{..} -> do
              -- TODO: We set up one script service context per file that
              -- we pass to execTest and script contexts are quite expensive.
              -- Therefore we keep the behavior of only passing the root file
              -- if source points to a specific file.
              files <- getDamlRootFiles pSrc
              execTest files allTests coverage color mbJUnitOutput (Just pkgConfig) cliOptions tableOutputPath transactionsOutputPath coveragePaths coverageFilters
  runTestsInPackageOrFiles packageLocationOpts (Just inFiles) allTests _ coverage color mbJUnitOutput cliOptions initPkgDb tableOutputPath transactionsOutputPath coveragePaths coverageFilters = Command Test (Just packageLocationOpts) effect
    where effect = withPackageRoot (packageRoot packageLocationOpts) (packageLocationCheck packageLocationOpts) $ \mPackageRoot relativize -> do
            cliOptions <- addResolutionData cliOptions
            cliOptions <- pure $ cliOptions { optMbPackageConfigPath = PackagePath <$> mPackageRoot }
            -- Cannot run without package context as resolution provides no default/project level resolution, so we wouldn't be able to find script-service
            when (isJust (optResolutionData cliOptions) && isNothing mPackageRoot) $
              throwIO $ DPMUnsupportedError "running tests outside of a package"
            installDepsAndInitPackageDb cliOptions initPkgDb
            mbJUnitOutput <- traverse relativize mbJUnitOutput
            mPkgConfig <- case mPackageRoot of
              Just packagePath -> withMaybeConfig (withPackageConfig (PackagePath packagePath)) pure
              Nothing -> pure Nothing
            inFiles' <- mapM (fmap toNormalizedFilePath' . relativize) inFiles
            execTest inFiles' allTests coverage color mbJUnitOutput mPkgConfig cliOptions tableOutputPath transactionsOutputPath coveragePaths coverageFilters
  
  cmdInspect :: Mod CommandFields Command
  cmdInspect =
      command "inspect" $ info (helper <*> cmd)
        $ progDesc "Pretty print a DALF file or the main DALF of a DAR file"
      <> fullDesc
    where
      jsonOpt = switch $ long "json" <> help "Output the raw Protocol Buffer structures as JSON"
      cmd = execInspect <$> inputFileOptWithExt ".dalf or .dar" <*> outputFileOpt <*> jsonOpt <*> cliOptDetailLevel
  
  cmdBuild :: SdkVersion.Class.SdkVersioned => Int -> Mod CommandFields Command
  cmdBuild numProcessors =
      command "build" $
      info (helper <*> cmdBuildParser numProcessors) $
      progDesc "Initialize, build and package the Daml package" <> fullDesc
  
  cmdBuildParser :: SdkVersion.Class.SdkVersioned => Int -> Parser Command
  cmdBuildParser numProcessors =
      execBuild
          <$> packageLocationOpts "daml build"
          <*> optionsParser
                numProcessors
                (EnableScriptService False)
                (pure Nothing)
                disabledDlintUsageParser
          <*> optionalOutputFileOpt
          <*> incrementalBuildOpt
          <*> initPkgDbOpt
          <*> enableMultiPackageOpt
          <*> multiPackageBuildAllOpt
          <*> multiPackageNoCacheOpt
          <*> multiPackageLocationOpt
  
  cmdClean :: Mod CommandFields Command
  cmdClean =
      command "clean" $
      info (helper <*> cmd) $
      progDesc "Remove Daml package build artifacts" <> fullDesc
    where
      cmd = execClean
              <$> packageLocationOpts "daml clean"
              <*> enableMultiPackageOpt
              <*> multiPackageLocationOpt
              <*> multiPackageCleanAllOpt
  
  cmdInit :: SdkVersion.Class.SdkVersioned => Int -> Mod CommandFields Command
  cmdInit numProcessors =
      command "init" $
      info (helper <*> cmd) $ progDesc "Initialize a Daml package" <> fullDesc
    where
      cmd = execInit
              <$> optionsParser
                    numProcessors
                    (EnableScriptService False)
                    (pure Nothing)
                    disabledDlintUsageParser
              <*> packageLocationOpts "daml damlc init"
  
  cmdInspectDar :: Mod CommandFields Command
  cmdInspectDar =
      command "inspect-dar" $
      info (helper <*> cmd) $ progDesc "Inspect a DAR archive" <> fullDesc
    where
      jsonOpt =
          flag InspectDar.PlainText InspectDar.Json $
          long "json" <> help "Output the information in JSON"
      cmd = execInspectDar
          <$> inputDarOpt
          <*> jsonOpt
  
  cmdValidateDar :: Mod CommandFields Command
  cmdValidateDar =
      command "validate-dar" $
      info (helper <*> cmd) $ progDesc "Validate a DAR archive" <> fullDesc
    where
      cmd = execValidateDar <$> inputDarOpt
  
  cmdDocTest :: SdkVersion.Class.SdkVersioned => Int -> Mod CommandFields Command
  cmdDocTest numProcessors =
      command "doctest" $
      info (helper <*> cmd) $
      progDesc "Early Access (Labs). doc tests" <> fullDesc
    where
      cmd = execDocTest
          <$> optionsParser
                numProcessors
                (EnableScriptService True)
                optPackageName
                disabledDlintUsageParser
          <*> strOptionOnce (long "script-lib" <> value "daml-script" <> internal)
              -- This is useful for tests and `bazel run`.
          <*> (ImportSource <$> flagYesNoAuto "import-source" True "Should source code directory be directly imported" internal)
              -- We need this when generating docs for stdlib, as we do not want to directly provide the source here,
              -- instead we use the already provided stdlib in the env. The reason for this is that Daml.Script relies on stdlib,
              -- and recompiling it alongside Daml.Script causes issues with missing interface files
          <*> many inputFileOpt
  
  cmdGenerateMultiPackageManifest :: SdkVersion.Class.SdkVersioned => Mod CommandFields Command
  cmdGenerateMultiPackageManifest =
      command "generate-multi-package-manifest" $
      info (helper <*> cmd) $
      progDesc "Generates the multi-package manifest file used by external build systems" <> fullDesc
    where
      cmd = execGenerateMultiPackageManifest
          <$> multiPackageLocationOpt
          <*> generateMultiPackageManifestOutputOpt
  
  cmdStudio :: Mod CommandFields Command
  cmdStudio =
      command "studio" $
      info (helper <*> cmd) $
      progDesc "Launch Daml Studio" <> fullDesc
    where
      cmd = execDamlStudio
          <$> studioReplaceOpt
          <*> many (strArgument $ metavar "ARG")
      execDamlStudio opts args = Command Studio Nothing $ runDamlStudio opts args
  
  --------------------------------------------------------------------------------
  -- Execution
  --------------------------------------------------------------------------------
  
  execLicense :: Command
  execLicense =
    Command License Nothing effect
    where
      effect = B.putStr licenseData
      licenseData :: B.ByteString
      licenseData = $(embedFile "NOTICES")
  
  execIde :: SdkVersion.Class.SdkVersioned
          => Telemetry
          -> Debug
          -> EnableScriptService
          -> StudioAutorunAllScripts
          -> Options
          -> Command
  execIde telemetry (Debug debug) enableScriptService autorunAllScripts options =
      Command Ide Nothing effect
    where effect = NS.withSocketsDo $ do
            let threshold =
                    if debug
                    then Logger.Debug
                    else Logger.Info
            loggerH <- Logger.IO.newIOLogger
              stderr
              (Just 5000)
              -- NOTE(JM): ^ Limit the message length to 5000 characters as VSCode
              -- performance will be significatly impacted by large log output.
              threshold
              "LanguageServer"
            damlCacheM <- lookupEnv damlCacheEnvVar
            damlPathM <- lookupEnv damlPathEnvVar
            let gcpConfig = Logger.GCP.GCPConfig
                    { gcpConfigTag = "ide"
                    , gcpConfigCachePath = damlCacheM
                    , gcpConfigDamlPath = damlPathM
                    }
                withLogger f = case telemetry of
                    TelemetryOptedIn ->
                      let logOfInterest prio = prio `elem` [Logger.Telemetry, Logger.Warning, Logger.Error] in
                      Logger.GCP.withGcpLogger gcpConfig logOfInterest loggerH $ \gcpStateM loggerH' -> do
                        whenJust gcpStateM $ \gcpState -> do
                          Logger.GCP.setOptIn gcpState
                          Logger.GCP.logMetaData gcpState
                        f loggerH'
                    TelemetryOptedOut -> Logger.GCP.withGcpLogger gcpConfig (const False) loggerH $ \gcpStateM loggerH -> do
                        whenJust gcpStateM $ \gcpState -> Logger.GCP.logOptOut gcpState
                        f loggerH
                    TelemetryIgnored -> Logger.GCP.withGcpLogger gcpConfig (const False) loggerH $ \gcpStateM loggerH -> do
                        whenJust gcpStateM $ \gcpState -> Logger.GCP.logIgnored gcpState
                        f loggerH
                    TelemetryDisabled -> f loggerH
            pkgPath <- getCanonDefaultPackagePath
            mPkgConfig <- withMaybeConfig (withPackageConfig pkgPath) pure
            let pkgConfUpgradeDar = pUpgradeDar =<< mPkgConfig
            options <- updateUpgradePath "ide" pkgPath options pkgConfUpgradeDar
            options <- addResolutionData options
            options <- pure options
                { optScriptService = enableScriptService
                , optEnableOfInterestRule = True
                , optSkipScriptValidation = SkipScriptValidation True
                -- TODO(MH): The `optionsParser` does not provide a way to skip
                -- individual options. As a stopgap we ignore the argument to
                -- --jobs.
                , optThreads = 0
                , optMbPackageName = pName <$> mPkgConfig
                , optMbPackageVersion = mPkgConfig >>= pVersion
                , optMbPackageConfigPath = Just pkgPath
                -- IDE was built around not passing the package name and version, assuming the unit-id is "main" everywhere
                -- For upgrade metadata checking, the IDE now needs the package name and version
                -- This flag allows us to still hide the unit-id from ghc, so our assumption that the unit-id is "main" holds.
                , optHideUnitId = True
                }
            installDepsAndInitPackageDb options (InitPkgDb True)
            scriptServiceConfig <- readScriptServiceConfig
            withLogger $ \loggerH ->
                withScriptService' enableScriptService (optDamlLfVersion options) loggerH scriptServiceConfig (scriptServiceJarFromOptions options) $ \mbScriptService -> do
                    sdkVersion <- getSdkVersion `catchIO` const (pure "Unknown (not started via the assistant)")
                    Logger.logInfo loggerH (T.pack $ "SDK version: " <> sdkVersion)
                    debouncer <- newAsyncDebouncer
                    runLanguageServer loggerH enabledPlugins Config $ \lspEnv vfs _ ->
                        getDamlIdeState options autorunAllScripts mbScriptService loggerH debouncer (RealLspEnv lspEnv) vfs
  
  
  -- | Whether we should write interface files during `damlc compile`.
  newtype WriteInterface = WriteInterface Bool
  
  execCompile :: SdkVersion.Class.SdkVersioned => FilePath -> FilePath -> Options -> WriteInterface -> Maybe FilePath -> Command
  execCompile inputFile outputFile opts (WriteInterface writeInterface) mbIfaceDir =
    Command Compile (Just packageLocationOpts) effect
    where
      packageLocationOpts = PackageLocationOpts Nothing (PackageLocationCheck "" False)
      effect = withPackageRoot' packageLocationOpts $ \relativize -> do
        loggerH <- getLogger opts "compile"
        inputFile <- toNormalizedFilePath' <$> relativize inputFile
        opts <- pure opts { optIfaceDir = mbIfaceDir }
        withDamlIdeState opts loggerH diagnosticsLogger $ \ide -> do
            setFilesOfInterest ide (HashSet.singleton inputFile)
            runActionSync ide $ do
              -- Support for '-ddump-parsed', '-ddump-parsed-ast', '-dsource-stats'.
              dflags <- hsc_dflags . hscEnv <$> use_ GhcSession inputFile
              parsed <- pm_parsed_source <$> use_ GetParsedModule inputFile
              liftIO $ do
                ErrUtils.dumpIfSet_dyn dflags Opt_D_dump_parsed "Parser" $ GHC.ppr parsed
                ErrUtils.dumpIfSet_dyn dflags Opt_D_dump_parsed_ast "Parser AST" $ showAstData NoBlankSrcSpan parsed
                ErrUtils.dumpIfSet_dyn dflags Opt_D_source_stats "Source Statistics" $ ppSourceStats False parsed
  
              when writeInterface $ do
                  files <- nubSort . concatMap transitiveModuleDeps <$> use GetDependencies inputFile
                  mbIfaces <- writeIfacesAndHie (toNormalizedFilePath' $ fromMaybe ifaceDir $ optIfaceDir opts) files
                  void $ liftIO $ mbErr "ERROR: Compilation failed." mbIfaces
  
              mbDalf <- getDalf inputFile
              dalf <- liftIO $ mbErr "ERROR: Compilation failed." mbDalf
              liftIO $ write dalf
      write bs
        | outputFile == "-" = putStrLn $ render Colored $ DA.Pretty.pretty bs
        | otherwise = do
          createDirectoryIfMissing True $ takeDirectory outputFile
          B.writeFile outputFile $ Archive.encodeArchive bs
  
  execDesugar :: SdkVersion.Class.SdkVersioned => FilePath -> FilePath -> Options -> Command
  execDesugar inputFile outputFile opts = Command Desugar (Just packageLocationOpts) effect
    where
      packageLocationOpts = PackageLocationOpts Nothing (PackageLocationCheck "" False)
      effect = withPackageRoot' packageLocationOpts $ \relativize ->
        liftIO . write =<< desugar opts =<< relativize inputFile
      write s
        | outputFile == "-" = T.putStrLn s
        | otherwise = do
          createDirectoryIfMissing True $ takeDirectory outputFile
          T.writeFile outputFile s
  
  execDebugIdeSpanInfo :: SdkVersion.Class.SdkVersioned => FilePath -> FilePath -> Options -> Command
  execDebugIdeSpanInfo inputFile outputFile opts =
    Command DebugIdeSpanInfo (Just packageLocationOpts) effect
    where
      packageLocationOpts = PackageLocationOpts Nothing (PackageLocationCheck "" False)
      effect =
        withPackageRoot' packageLocationOpts $ \relativize -> do
          loggerH <- getLogger opts "debug-ide-span-info"
          inputFile <- toNormalizedFilePath' <$> relativize inputFile
          spanInfo <- withDamlIdeState opts loggerH diagnosticsLogger $ \ide -> do
            setFilesOfInterest ide (HashSet.singleton inputFile)
            runActionSync ide $ do
              dflags <- hsc_dflags . hscEnv <$> use_ GhcSession inputFile
              getSpanInfo dflags inputFile
          write $ Aeson.encodeToLazyText spanInfo
      write s
        | outputFile == "-" = TL.putStrLn s
        | otherwise = do
          createDirectoryIfMissing True $ takeDirectory outputFile
          TL.writeFile outputFile s
  
  execLint :: SdkVersion.Class.SdkVersioned => [FilePath] -> Options -> Command
  execLint inputFiles opts =
    Command Lint (Just packageLocationOpts) effect
    where
       packageLocationOpts = PackageLocationOpts Nothing (PackageLocationCheck "" False)
       effect =
         withPackageRoot' packageLocationOpts $ \relativize ->
         do
           loggerH <- getLogger opts "lint"
           withDamlIdeState opts loggerH diagnosticsLogger $ \ide -> do
               inputFiles <- getInputFiles relativize inputFiles
               setFilesOfInterest ide (HashSet.fromList inputFiles)
               diags <- forM inputFiles $ \inputFile -> do
                 void $ runActionSync ide $ getDlintIdeas inputFile
                 getDiagnostics ide
               if all null diags then
                 hPutStrLn stderr "No hints"
               else
                 exitFailure
       getInputFiles relativize = \case
         [] -> do
             withPackageConfig defaultPackagePath $ \PackageConfigFields {pSrc} -> do
             getDamlRootFiles pSrc
         fs -> forM fs $ fmap toNormalizedFilePath' . relativize
  
  defaultPackagePath :: PackagePath
  defaultPackagePath = PackagePath "."
  
  getCanonDefaultPackagePath :: IO PackagePath
  getCanonDefaultPackagePath = fmap PackagePath $ canonicalizePath $ unwrapPackagePath defaultPackagePath
  
  -- | If we're in a daml package, read the daml.yaml field, install the dependencies and create the
  -- package local package database. Otherwise do nothing.
  execInit :: SdkVersion.Class.SdkVersioned => Options -> PackageLocationOpts -> Command
  execInit opts packageLocationOpts =
    Command Init (Just packageLocationOpts) effect
    where effect = withPackageRoot' packageLocationOpts $ \_relativize ->
            installDepsAndInitPackageDb
              opts
              (InitPkgDb True)
  
  installDepsAndInitPackageDb :: SdkVersion.Class.SdkVersioned => Options -> InitPkgDb -> IO ()
  installDepsAndInitPackageDb opts (InitPkgDb shouldInit) =
      when shouldInit $ do
          isPackage <- withPackageConfig defaultPackagePath (const $ pure True) `catch` (\(_ :: ConfigError) -> pure False)
          when isPackage $ do
              packageRoot <- getCurrentDirectory
              opts <- addResolutionData opts
              withPackageConfig defaultPackagePath $
                setupPackageDbFromPackageConfig (toNormalizedFilePath' packageRoot) opts
  
  getMultiPackagePath :: MultiPackageLocation -> Maybe FilePath -> IO (Maybe PackagePath)
  getMultiPackagePath multiPackageLocation searchPath =
    case multiPackageLocation of
      MPLSearch -> findMultiPackageConfig $ maybe defaultPackagePath PackagePath searchPath
      MPLPath path -> do
        hasMultiPackage <- doesFileExist $ path </> multiPackageConfigName
        unless hasMultiPackage $ do
          hPutStrLn stderr $ (path </> multiPackageConfigName) <> " does not exist."
          exitFailure
        pure $ Just $ PackagePath path
  
  execBuild
    :: SdkVersion.Class.SdkVersioned
    => PackageLocationOpts
    -> Options
    -> Maybe FilePath
    -> IncrementalBuild
    -> InitPkgDb
    -> EnableMultiPackage
    -> MultiPackageBuildAll
    -> MultiPackageNoCache
    -> MultiPackageLocation
    -> Command
  execBuild packageLocationOpts opts mbOutFile incrementalBuild initPkgDb enableMultiPackage buildAll noCache multiPackageLocation =
    Command Build (Just packageLocationOpts) $ evalContT $ do
      -- Need exec path for `daml build --all`, where we don't want to be relativized to a package
      execPath <- liftIO getCurrentDirectory
  
      relativize <- ContT $ withPackageRoot' (packageLocationOpts {packageLocationCheck = PackageLocationCheck "" False})
  
      opts <- liftIO $ addResolutionData opts
  
      let buildSingle :: PackagePath -> PackageConfigFields -> IO ()
          buildSingle pkgPath pkgConfig = void $ buildEffect relativize pkgPath pkgConfig opts mbOutFile incrementalBuild initPkgDb
          buildMulti :: PackagePath -> Maybe PackageConfigFields -> PackagePath -> IO ()
          buildMulti pkgPath mPkgConfig multiPackageConfigPath = do
            hPutStrLn stderr $ "Running multi-package build of "
              <> maybe ("all packages in " <> unwrapPackagePath multiPackageConfigPath) (T.unpack . LF.unPackageName . pName) mPkgConfig <> "."
            withMultiPackageConfig multiPackageConfigPath $ \multiPackageConfig ->
              multiPackageBuildEffect relativize pkgPath mPkgConfig multiPackageConfig opts mbOutFile incrementalBuild initPkgDb noCache
  
      pkgPath <- liftIO getCanonDefaultPackagePath
      mPkgConfig <- ContT $ withMaybeConfig $ withPackageConfig pkgPath
      liftIO $ if getEnableMultiPackage enableMultiPackage then do
        -- If running build --all, search from invocation location, as package location is irrelevant
        mMultiPackagePath <- getMultiPackagePath multiPackageLocation $ if getMultiPackageBuildAll buildAll then Just execPath else Nothing
        -- At this point, if mMultiPackagePath is Just, we know it points to a multi-package.yaml
  
        case (getMultiPackageBuildAll buildAll, mPkgConfig, mMultiPackagePath) of
          -- We're attempting to multi-package build --all, so we require that we have a multi-package.yaml, but do not care if we have a daml.yaml
          (True, _, Just multiPackagePath) ->
            -- TODO[SW]: Ideally we would error here if any of the flags that change `opts` has been set, as it won't be propagated
            -- Its unclear how to implement this.
            buildMulti pkgPath Nothing multiPackagePath
  
          -- We're attempting to multi-package build --all but we don't have a multi-package.yaml
          (True, _, Nothing) -> do
            hPutStrLn stderr
              "Attempted to build all packages, but could not find a multi-package.yaml at current or parent directory. Use --multi-package-path to specify its location."
            exitFailure
  
          -- We know the package we want and we have a multi-package.yaml
          (False, Just pkgConfig, Just multiPackagePath) -> buildMulti pkgPath (Just pkgConfig) multiPackagePath
  
          -- We know the package we want but we do not have a multi-package. The user has provided no reason they would want a multi-package build.
          (False, Just pkgConfig, Nothing) -> do
            hPutStrLn stderr $ "Running single package build of " <> T.unpack (LF.unPackageName $ pName pkgConfig) <> " as no multi-package.yaml was found."
            buildSingle pkgPath pkgConfig
  
          -- We have no package context, but we have found a multi package at the current directory
          (False, Nothing, Just _) -> do
            hPutStrLn stderr $ "No valid daml.yaml found, but a multi-package.yaml was found instead.\n"
              <> "If you intended to build everything within this multi-package.yaml, use the --all flag"
            exitFailure
  
          -- We have nothing, we're lost
          (False, Nothing, Nothing) -> do
            hPutStrLn stderr "No valid daml.yaml or multi-package.yaml could be found."
            exitFailure
      else
        case mPkgConfig of
          Nothing -> do
            hPutStrLn stderr "daml build: Not in package."
            exitFailure
          Just pkgConfig -> do
            let usedMultiPackageOption =
                  getMultiPackageBuildAll buildAll
                    || getMultiPackageNoCache noCache
                    || multiPackageLocation /= MPLSearch
            if usedMultiPackageOption
              then do
                hPutStrLn stderr "Multi-package build option used with multi-package disabled - re-enable it by setting the --enable-multi-package=yes flag."
                exitFailure
              else buildSingle pkgPath pkgConfig
  
  -- Takes the withPackageConfig style functions and changes the continuation
  -- to give a Maybe, where Nothing signifies a missing file. Parse errors are still thrown
  withMaybeConfig
    :: forall config x
    .  (forall y. (config -> IO y) -> IO y)
    -> (Maybe config -> IO x)
    -> IO x
  withMaybeConfig withConfig handler = do
    mConfig <-
      handle (\case
        ConfigFileInvalid _ (Y.InvalidYaml (Just (Y.YamlException exc))) | "Yaml file not found: " `isPrefixOf` exc ->
          pure Nothing
        ConfigFileInvalid _ (Y.InvalidYaml (Just (Y.YamlException exc))) | "packageless daml.yaml" `isInfixOf` exc -> do
          hPutStrLn stderr "Found daml.yaml with only sdk-version, ignoring this file."
          pure Nothing
        e -> throwIO e
      ) (withConfig $ pure . Just)
    handler mConfig
  
  buildEffect
    :: SdkVersion.Class.SdkVersioned
    => (FilePath -> IO FilePath)
    -> PackagePath
    -> PackageConfigFields
    -> Options
    -> Maybe FilePath
    -> IncrementalBuild
    -> InitPkgDb
    -> IO (Maybe LF.PackageId)
  buildEffect relativize pkgPath pkgConfig opts mbOutFile incrementalBuild initPkgDb = do
    (pkgConfig, opts) <- syncUpgradesField pkgPath pkgConfig opts
    let PackageConfigFields{..} = pkgConfig
    installDepsAndInitPackageDb opts initPkgDb
    loggerH <- getLogger opts "build"
    Logger.logInfo loggerH $ "Compiling " <> LF.unPackageName pName <> " to a DAR."
    let errors = checkPkgConfig pkgConfig
    unless (null errors) $ do
        mapM_ (Logger.logError loggerH) errors
        exitFailure
    withDamlIdeState
        opts
          { optMbPackageName = Just pName
          , optMbPackageVersion = pVersion
          , optMbPackageConfigPath = Just pkgPath
          , optIncrementalBuild = incrementalBuild
          }
        loggerH
        diagnosticsLogger $ \compilerH -> do
        fp <- targetFilePath relativize $ unitIdString (pkgNameVersion pName pVersion)
        mbDar <-
            buildDar
                compilerH
                pkgConfig
                (toNormalizedFilePath' $ fromMaybe ifaceDir $ optIfaceDir opts)
                (FromDalf False)
                (optUpgradeInfo opts)
                (optTypecheckerWarningFlags opts)
                (Just pkgPath)
        (dar, mPkgId) <- mbErr "ERROR: Creation of DAR file failed." mbDar
        createDarFile loggerH fp dar
        pure mPkgId
      where
          targetFilePath rel name =
            case mbOutFile of
              Nothing -> pure $ distDir </> name <.> "dar"
              Just out -> rel out
  
          syncUpgradesField :: PackagePath -> PackageConfigFields -> Options -> IO (PackageConfigFields, Options)
          syncUpgradesField pkgPath pkgConf opts = do
            opts <- updateUpgradePath "build" pkgPath opts (pUpgradeDar pkgConf)
            pure (pkgConf { pUpgradeDar = uiUpgradedPackagePath (optUpgradeInfo opts) }, opts)
  
  updateUpgradePath :: T.Text -> PackagePath -> Options -> Maybe FilePath -> IO Options
  updateUpgradePath context packagePath opts@Options{optUpgradeInfo} newPkgPath = do
    let packageFilePath = unwrapPackagePath packagePath
    optCanonPath <- traverse canonicalizePath $ uiUpgradedPackagePath optUpgradeInfo
    pkgCanonPath <- withCurrentDirectory packageFilePath $ traverse canonicalizePath newPkgPath
    -- optUpgradeInfo is normalised to current directory
    -- newPkgPath is normalised to daml.yaml location (or currentDirectory)
    uiUpgradedPackagePath <- case (pkgCanonPath, optCanonPath) of
      (Just damlYamlOption, Just buildFlagsOption) | damlYamlOption /= buildFlagsOption -> do
        loggerH <- getLogger opts context
        Logger.logError loggerH $ T.unlines
          [ "ERROR: Specified two different DARs to run upgrade checks against:"
          , "  Path specified in daml.yaml `upgrades:` field is '" <> T.pack (makeRelative packageFilePath damlYamlOption) <> "'"
          , "  Path specified in `--upgrades` build option is '" <> T.pack (makeRelative packageFilePath buildFlagsOption) <> "'"
          ]
        exitFailure
      (a, b) ->
        pure (a <|> b)
    pure $ opts { optUpgradeInfo = optUpgradeInfo { uiUpgradedPackagePath } }
  
  
  {- | Multi Build! (the multi-package.yaml approach)
    This is currently separate to build, but won't be in future.
    Multi build is a light wrapper over the top of the existing build, and does not interact with the internals of the build process at all
    It spins up its own empty shake environment purely for the caching behaviour, and defers as soon as possible to the usual build process
    We strictly defer via the CLI interface, to support different versions of the SDK in future.
  
    The general idea is taken somewhat from cabal, in which we define some kind of project file (currently multi-package.yaml)
    which lists directories to packages (folders containing a daml.yaml) that are in our project, and we'd like to rebuild together.
    Then, any data-dependency on one of these packages will first ensure the dar artefact is up to date with the source, and rebuild if not.
    As such, any set of packages with these relations will continue to be buildable without the multi-package.yaml.
    Given we may want to build specific packages within our dependency multi-tree (be that a sub tree, or an alternate root),
    the multi-package.yaml is not required to be in the same directory as any given package. We only require it is at or above
    (in the directory tree) the package you are building. multi-build will first look for this file before attempting to build.
  
    The process is as follows:
    - Search for the multi-package.yaml file by traversing up the directory tree to root until we are at a directory containing the file.
    - Set up own shake environment, defer to it immediately with the package path
    - Within the shake rule:
    - Partially read the daml.yaml file, looking for data dependencies, and some other details needed for caching
    - Intercept the packages data dependencies with the packages we have listed in multi-package.yaml.
    - Call to shake to build all the above subset of data-dependencies, each will return data about the DAR that exists (or was built)
    - If we already have a DAR, check its staleness. Staleness is defined by any source files being different
        or any of the package ids of souce dependencies being different between what is currently built, and what is embedded in our dar.
    - If either are different, call to the SDK via CLI to build our own DAR.
    - Get the package ID of the DAR we have created, and return it to shake, which can use it as a caching key.
  
    Due to shakes caching behaviour, this will automatically discover the build order for free, by doing a depth first search over the tree then
      caching any "nodes" we've already seen entirely within Shake - our rule will not be called twice for any package within the tree for any given multi-build.
  
    We currently rely on shake to find cycles, how its errors includes too much information about internals, so we'll need to implement our own cycle detection.
  -}
  multiPackageBuildEffect
    :: SdkVersion.Class.SdkVersioned
    => (FilePath -> IO FilePath)
    -> PackagePath
    -> Maybe PackageConfigFields -- Nothing signifies build all
    -> MultiPackageConfigFields
    -> Options
    -> Maybe FilePath
    -> IncrementalBuild
    -> InitPkgDb
    -> MultiPackageNoCache
    -> IO ()
  multiPackageBuildEffect relativize pkgPath mPkgConfig multiPackageConfig opts mbOutFile incrementalBuild initPkgDb noCache = do
    vfs <- makeVFSHandle
    loggerH <- getLogger opts "multi-package build"  
  
    buildableDataDepsMapping <- fmap Map.fromList $ for (mpPackagePaths multiPackageConfig) $ \path -> do
      darPath <- darPathFromDamlYaml path
      pure (darPath, path)
  
    getDamlcPath <-
      case optResolutionData opts of
        Just resolutionData -> pure $ \location ->
          case findPackageResolutionData location resolutionData of
            Right validPkgResolution ->
              case Map.lookup "damlc-binary" $ imports validPkgResolution of
                Just [damlcLocation] -> pure damlcLocation
                _ -> throwIO $ ResolutionError $ "Damlc could not be found in DPM resolution for " <> location <> ". You SDK install for this package is invalid."
            Left err -> throwIO err
        Nothing -> do
          mbAssistantPath <- lookupEnv "DAML_ASSISTANT"
          case mbAssistantPath of
            Just assistantPath -> pure $ const $ pure assistantPath
            Nothing -> error "Couldn't find DPM or Daml Assistant. Please run damlc via one of these package managers."
  
    -- Must drop DAML_PACKAGE + DAML_PROJECT from env var so it can be repopulated based on `cwd`
    damlcEnv <- filter (flip notElem [projectPathEnvVar, packagePathEnvVar, "DAML_SDK_VERSION", "DAML_SDK"] . fst) <$> getEnvironment
  
    let damlcRunner = DamlcRunner $ \location args -> do
          damlcPath <- getDamlcPath location
          withCreateProcess ((proc damlcPath args) {cwd = Just location, env = Just damlcEnv}) $ \_ _ _ p -> do
            exitCode <- waitForProcess p
            when (exitCode /= ExitSuccess) $ error $ "Failed to build package at " <> location <> "."
  
        buildableDataDeps = BuildableDataDeps $ flip Map.lookup buildableDataDepsMapping
        mRootPkgBuilder = flip fmap mPkgConfig $ \pkgConfig -> do
          mPkgId <- buildEffect relativize pkgPath pkgConfig opts mbOutFile incrementalBuild initPkgDb
          pure $ fromMaybe
            (error "Internal error: root package was built from dalf, giving no package-id. This is incompatible with multi-package")
            mPkgId
        mRootPkgData = (toNormalizedFilePath' $ unwrapPackagePath pkgPath,) <$> mRootPkgBuilder
        rule = buildMultiRule damlcRunner buildableDataDeps noCache mRootPkgData
  
    -- Set up a near empty shake environment, with just the buildMulti rule
    bracket
      (IDE.initialise rule (DummyLspEnv diagnosticsLogger) (toIdeLogger loggerH) noopDebouncer (toCompileOpts opts) vfs)
      IDE.shutdown
      $ \ideState -> runActionSync ideState
        $ case mRootPkgData of
            -- `uses_` tries to run in "parallel", but ends up just running backwards for multi-build.
            -- We use `reverse` here to keep the build order consistent with the multi-package.yaml
            Nothing -> void $ uses_ BuildMulti $ reverse $ toNormalizedFilePath' <$> mpPackagePaths multiPackageConfig
            Just (rootPkgPath, _) -> void $ use_ BuildMulti rootPkgPath
  
  -- This runs damlc for a given package path.
  -- With legacy daml assistant, it simply calls out to the assistant CLI and allows it to resolve the damlc
  -- With DPM, it looks through the resolution file for the given package and calls the correct damlc-binary directly
  data DamlcRunner = DamlcRunner { runDamlc :: FilePath -> [String] -> IO ()}
  
  -- Stores a mapping from dar path to package path
  data BuildableDataDeps = BuildableDataDeps { getDataDepSource :: FilePath -> Maybe FilePath }
  
  data BuildMulti = BuildMulti
      deriving (Eq, Show, Typeable, Generic)
  instance Binary BuildMulti
  instance Hashable BuildMulti
  instance NFData BuildMulti
  
  -- We return all the information needed to derive the dalf name
  type instance RuleResult BuildMulti = (LF.PackageName, LF.PackageVersion, LF.PackageId)
  
  -- Subset of PackageConfig needed for multi-package build deferring.
  data BuildMultiPackageConfig = BuildMultiPackageConfig
    { bmSdkVersion :: UnresolvedReleaseVersion
    , bmName :: LF.PackageName
    , bmVersion :: LF.PackageVersion
    , bmDarDeps :: [FilePath]
        -- ^ not canonicalized, all dars that the package requires to build, including "upgrades"
    , bmSourceDaml :: FilePath
        -- ^ not canonicalized
    , bmOutput :: Maybe FilePath
        -- ^ not canonicalized
    }
  
  -- Version of getDamlFiles from Dar.hs that assumes source is a folder, and runs in IO rather than action
  -- Warns and gives an empty map when source is a file
  getDamlFilesBuildMulti :: (String -> IO ()) -> FilePath -> FilePath -> IO (Map.Map FilePath BSL.ByteString)
  getDamlFilesBuildMulti log packagePath srcDir = do
    let path = normalise $ packagePath </> srcDir
    exists <- doesDirectoryExist path
    if exists then do
      files <- fmap fromNormalizedFilePath <$> damlFilesInDir path
      Map.fromList <$> traverse (\file -> (makeRelative path file,) <$> BSL.readFile file) files
    else do
      log $ "WARNING: Multi-package caching does not support setting the daml.yaml source value to a daml file. This package will always rebuild.\n"
        <> "Change the source value in "
        <> (packagePath </> packageConfigName)
        <> " to a directory to allow correct caching.\n"
      pure Map.empty
  
  -- Extract the name/version string and package ID of a given dalf in a dar.
  entryToDalfData :: ZipArchive.Entry -> Maybe (String, LF.PackageId)
  entryToDalfData entry = do
    let fileName = takeBaseName $ ZipArchive.eRelativePath entry
    lastDash <- listToMaybe $ reverse $ elemIndices '-' fileName
    pure $ second (LF.PackageId . T.pack . tail) $ splitAt lastDash fileName
  
  dalfDataKey :: LF.PackageName -> LF.PackageVersion -> String
  dalfDataKey name version = T.unpack (LF.unPackageName name <> "-" <> LF.unPackageVersion version)
  
  -- Given an archive, returns the packagename-packageversion string with package ID for all dalfs, and the file path and file contents for all daml files.
  readDarStalenessData :: ZipArchive.Archive -> (Map.Map String LF.PackageId, Map.Map FilePath BSL.ByteString)
  readDarStalenessData archive =
    bimap Map.fromList Map.fromList . partitionEithers $
      flip mapMaybe (ZipArchive.zEntries archive) $ \entry ->
        case takeExtension $ ZipArchive.eRelativePath entry of
          ".dalf" -> Left <$> entryToDalfData entry
          ".daml" ->
            -- Drop the first directory in the path, as thats the folder containing the entire package contents.
            -- Previously structured this way to support multiple packages in one dar.
            let relPackagePath = joinPath $ tail $ splitPath $ ZipArchive.eRelativePath entry
             in Just $ Right (relPackagePath, ZipArchive.fromEntry entry)
          _ -> Nothing
  
  getDarSdkVersion :: ZipArchive.Archive -> Either String String
  getDarSdkVersion dar = Reader.sdkVersion <$> readDalfManifest dar
  
  -- | Gets the package id of a Dar in the given package ONLY if it is not stale.
  getPackageIdIfFresh :: IDELogger.Logger -> FilePath -> BuildMultiPackageConfig -> [(LF.PackageName, LF.PackageVersion, LF.PackageId)] -> IO (Maybe LF.PackageId)
  getPackageIdIfFresh logger path BuildMultiPackageConfig {..} sourceDepPids = do
    let logDebug str = IDELogger.logDebug logger $ T.pack $ path <> ": " <> str
        logInfo str = IDELogger.logInfo logger $ T.pack str
    darPath <- deriveDarPath path bmName bmVersion bmOutput
    exists <- doesFileExist darPath
    if not exists
      then do
        logDebug "No DAR found, build is stale."
        pure Nothing
      else do
        logDebug "DAR found, checking staleness."
        -- Get the real source files we expect to be included in the dar
        sourceFiles <- getDamlFilesBuildMulti logInfo path bmSourceDaml
  
        dar <- ZipArchive.toArchive <$> BSL.readFile darPath
  
        -- Pull all information we need from the dar.
        let (archiveDalfPids, archiveSourceFiles) = readDarStalenessData dar
            archiveSdkVersion = getDarSdkVersion dar
  
            getArchiveDalfPid :: LF.PackageName -> LF.PackageVersion -> Maybe LF.PackageId
            getArchiveDalfPid name version = dalfDataKey name version `Map.lookup` archiveDalfPids
            -- We check source files by comparing the maps, any extra, missing or changed files will be covered by this, regardless of order
            sourceFilesCorrect = sourceFiles == archiveSourceFiles
            sdkVersionCorrect = archiveSdkVersion == Right (unresolvedReleaseVersionToString bmSdkVersion)
  
        -- Expanded `all` check that allows us to log specific dependencies being stale. Useful for debugging, maybe we can remove it.
        sourceDepsCorrect <-
          allM (\(name, version, pid) -> do
            let archiveDalfPid = getArchiveDalfPid name version
                valid = archiveDalfPid == Just pid
  
            when (not valid) $ logDebug $
              "Source dependency \"" <> dalfDataKey name version <> "\" is stale. Expected PackageId "
                <> T.unpack (LF.unPackageId pid) <> " but got " <> (show $ LF.unPackageId <$> archiveDalfPid) <> "."
  
            pure valid
          ) sourceDepPids
  
        logDebug $ "Source dependencies are " <> (if sourceDepsCorrect then "not " else "") <> "stale."
        logDebug $ "Source files are " <> (if sourceFilesCorrect then "not " else "") <> "stale."
        logDebug $ "Sdk version is " <> (if sdkVersionCorrect then "not " else "") <> "stale."
        pure $ if sourceDepsCorrect && sourceFilesCorrect && sdkVersionCorrect then getArchiveDalfPid bmName bmVersion else Nothing
  
  damlMultiBuildVersion :: UnresolvedReleaseVersion
  damlMultiBuildVersion = either throw id $ parseUnresolvedVersion "2.8.0-snapshot.20231018.0"
  
  buildMultiRule
    :: DamlcRunner
    -> BuildableDataDeps
    -> MultiPackageNoCache
    -> Maybe (NormalizedFilePath, IO LF.PackageId)
    -> Rules ()
  buildMultiRule damlcRunner buildableDataDeps (MultiPackageNoCache noCache) mRootPackage =
    defineEarlyCutoffWithDefaultRunChanged $ \BuildMulti path -> do
      logger <- actionLogger
  
      liftIO $ IDELogger.logDebug logger $ T.pack $ "Considering building: " <> fromNormalizedFilePath path
  
      let filePath = fromNormalizedFilePath path
      bmPkgConfig@BuildMultiPackageConfig {..} <- liftIO $ buildMultiPackageConfigFromDamlYaml filePath
  
      -- Wrap up a PackageId and changed flag into the expected Shake return structure
      let makeReturn :: LF.PackageId -> Bool -> (Maybe B.ByteString, RunChanged, IdeResult (LF.PackageName, LF.PackageVersion, LF.PackageId))
          makeReturn pid changed =
            ( Just $ encodeUtf8 $ LF.unPackageId pid
            , if changed then ChangedRecomputeDiff else ChangedRecomputeSame
            , ([], Just (bmName, bmVersion, pid))
            )
  
      toBuild <- liftIO $ withCurrentDirectory filePath $ flip mapMaybeM bmDarDeps $ \darPath -> do
        canonDarPath <- canonicalizePath darPath
        pure $ getDataDepSource buildableDataDeps canonDarPath
  
      -- Build all our deps!
      sourceDepsData <- uses_ BuildMulti $ toNormalizedFilePath' <$> toBuild
  
      liftIO $ case mRootPackage of
        -- We're building the root package, we'll envoke the normal builder directly with the Options already applied
        Just (pkgRootPath, builder) | path == pkgRootPath -> do
          IDELogger.logInfo logger $ T.pack $ "Building " <> filePath
          pkgId <- builder
          pure $ makeReturn pkgId True
        _ -> do
          -- Check our own staleness, if we're not stale, give the package ID to return to shake.
          -- If caching disabled, we fail this check pre-emtively
          ownValidPid <-
            if noCache
              then pure Nothing
              else getPackageIdIfFresh logger filePath bmPkgConfig sourceDepsData
  
          case ownValidPid of
            Just pid -> do
              IDELogger.logInfo logger $ T.pack $ filePath <> " is unchanged."
              pure $ makeReturn pid False
            Nothing -> do
              IDELogger.logInfo logger $ T.pack $ "Building " <> filePath
  
              -- Call build via daml assistant so it selects the correct SDK version.
              runDamlc damlcRunner filePath $
                ["build"] <> (["--enable-multi-package=no" | bmSdkVersion >= damlMultiBuildVersion || isHeadVersion bmSdkVersion])
  
              darPath <- deriveDarPath filePath bmName bmVersion bmOutput
              -- Extract the new package ID from the dar we just built, by reading the DAR and looking for the dalf that matches our package name/version
              archive <- ZipArchive.toArchive <$> BSL.readFile darPath
              let archiveDalfPids = Map.fromList $ mapMaybe entryToDalfData $ ZipArchive.zEntries archive
                  mOwnPid = dalfDataKey bmName bmVersion `Map.lookup` archiveDalfPids
                  ownPid = fromMaybe (error "Failed to get PID of own package - implies a DAR was built without its own dalf, something broke!") mOwnPid
  
              pure $ makeReturn ownPid True
  
  -- | Remove any build artifacts if they exist.
  execClean :: PackageLocationOpts -> EnableMultiPackage -> MultiPackageLocation -> MultiPackageCleanAll -> Command
  execClean packageLocationOpts enableMultiPackage multiPackageLocation cleanAll =
    Command Clean (Just packageLocationOpts) effect
    where
      effect
        | getEnableMultiPackage enableMultiPackage
        = do
          execPath <- liftIO getCurrentDirectory
          mMultiPackagePath <- getMultiPackagePath multiPackageLocation $ if getMultiPackageCleanAll cleanAll then Just execPath else Nothing
          isPackage <- withPackageRoot' (packageLocationOpts {packageLocationCheck = PackageLocationCheck "" False}) $ const $ doesFileExist packageConfigName
          case (mMultiPackagePath, isPackage, getMultiPackageCleanAll cleanAll) of
            -- daml clean --all with no multi-package.yaml
            (Nothing, _, True) -> do
              hPutStrLn stderr "Couldn't find a multi-package.yaml at current or parent directory. Use --multi-package-path to specify its location."
              exitFailure
            -- daml clean --all with a multi-package.yaml
            (Just path, _, True) ->
              withMultiPackageConfig path $ \multiPackageConfig -> do
                hPutStrLn stderr $ "Running multi-package clean of all packages in " <> unwrapPackagePath path
                forM_ (mpPackagePaths multiPackageConfig) $ \p ->
                  singleCleanEffect $ packageLocationOpts {packageRoot = Just $ PackagePath p}
                hPutStrLn stderr "Removed build artifacts."
            -- daml clean in a package
            (_, True, False) -> do
              singleCleanEffect packageLocationOpts
              hPutStrLn stderr "Removed build artifacts."
            -- daml clean outside a package and no multi-package.yaml
            (Nothing, False, False) -> do
              hPutStrLn stderr "No valid daml.yaml or multi-package.yaml could be found."
              exitFailure
            -- daml clean outside a package with a multi-package.yaml
            (Just _, False, False) -> do
              hPutStrLn stderr $ "No valid daml.yaml found, but a multi-package.yaml was found instead.\n"
                <> "If you intended to clean everything within this multi-package.yaml, use the --all flag"
              exitFailure
        | getMultiPackageCleanAll cleanAll
        = do
          hPutStrLn stderr "Multi-package clean option used with multi-package disabled - re-enable it by setting the --enable-multi-package=yes flag."
          exitFailure
        | otherwise = do
          singleCleanEffect packageLocationOpts
          hPutStrLn stderr "Removed build artifacts."
  
  singleCleanEffect :: PackageLocationOpts -> IO ()
  singleCleanEffect packageLocationOpts =
    withPackageRoot' packageLocationOpts $ \_relativize -> do
      isPackage <- doesFileExist packageConfigName
      when isPackage $ do
        canonDir <- getCurrentDirectory
        hPutStrLn stderr $ "Cleaning " <> canonDir
        whenM (doesPathExist damlArtifactDir) $
          removePathForcibly damlArtifactDir
  
  -- | Given a path to a .dalf or a .dar return the bytes of either the .dalf file
  -- or the main dalf from the .dar
  -- In addition to the bytes, we also return the basename of the dalf file.
  getDalfBytes :: FilePath -> IO (B.ByteString, FilePath)
  getDalfBytes fp
    | "dar" `isExtensionOf` fp = do
          dar <- B.readFile fp
          let archive = ZipArchive.toArchive $ BSL.fromStrict dar
          manifest <- either fail pure $ readDalfManifest archive
          dalfs <- either fail pure $ readDalfs archive
          pure (BSL.toStrict $ mainDalf dalfs, takeBaseName $ mainDalfPath manifest)
    | otherwise = (, takeBaseName fp) <$> B.readFile fp
  
  execInspect :: FilePath -> FilePath -> Bool -> DA.Pretty.PrettyLevel -> Command
  execInspect inFile outFile jsonOutput lvl =
    Command Inspect Nothing effect
    where
      effect = do
        (bytes, _) <- getDalfBytes inFile
        if jsonOutput
        then do
          payloadBytes <- PLF.archivePayload <$> errorOnLeft "Cannot decode archive" (PS.fromByteString bytes)
          archive :: PLF.ArchivePayload <- errorOnLeft "Cannot decode archive payload" $ PS.fromByteString payloadBytes
          writeOutputBSL outFile
           $ Aeson.Pretty.encodePretty
           $ Proto.JSONPB.toAesonValue archive
        else do
          (pkgId, lfPkg) <- errorOnLeft "Cannot decode package" $
                     Archive.decodeArchive Archive.DecodeAsMain bytes
          writeOutput outFile $ render Plain $
            DA.Pretty.vcat
              [ DA.Pretty.keyword_ "package" DA.Pretty.<-> DA.Pretty.text (LF.unPackageId pkgId)
              , DA.Pretty.pPrintPrec lvl 0 lfPkg
              ]
  
  errorOnLeft :: Show a => String -> Either a b -> IO b
  errorOnLeft desc = \case
    Left err -> ioError $ userError $ unlines [ desc, show err ]
    Right x  -> return x
  
  execInspectDar :: FilePath -> InspectDar.Format -> Command
  execInspectDar inFile jsonOutput =
    Command InspectDar Nothing (InspectDar.inspectDar inFile jsonOutput)
  
  execValidateDar :: FilePath -> Command
  execValidateDar inFile =
    Command ValidateDar Nothing effect
    where
      effect = do
        n <- validateDar inFile -- errors if validation fails
        putStrLn $ "DAR is valid; contains " <> show n <> " packages."
  
  -- | Should source files for doc test be imported into the test package (default yes)
  newtype ImportSource = ImportSource Bool
  
  execDocTest :: SdkVersion.Class.SdkVersioned => Options -> FilePath -> ImportSource -> [FilePath] -> Command
  execDocTest opts scriptDar (ImportSource importSource) files =
    Command DocTest Nothing effect
    where
      effect = do
        let files' = map toNormalizedFilePath' files
            packageFlag =
              ExposePackage
                ("--package daml-script-" <> SdkVersion.Class.sdkPackageVersion)
                (UnitIdArg $ stringToUnitId $ "daml-script-" <> SdkVersion.Class.sdkPackageVersion)
                (ModRenaming True [])
  
        logger <- getLogger opts "doctest"
  
        damlAssistantIsSet <- damlAssistantIsSet
        releaseVersion <- if damlAssistantIsSet
            then do
              damlPath <- getDamlPath
              damlEnv <- getDamlEnv damlPath (LookForPackagePath False)
              wrapErr "running doc test" $
                resolveReleaseVersionUnsafe (envUseCache damlEnv) SdkVersion.Class.unresolvedBuiltinSdkVersion
            else pure (unsafeResolveReleaseVersion SdkVersion.Class.unresolvedBuiltinSdkVersion)
        opts <- addResolutionData opts
  
        -- Can't yet support doc test as we have no default resolution, and doctest does not assume package context
        when (isJust (optResolutionData opts)) $
          throwIO $ DPMUnsupportedError "doctest"
  
        -- Install daml-script into their package-db
        -- Note this installs directly to the users dependency database, giving this command side effects.
        -- An approach of copying out the deps into a temporary location to build/run the tests has been considered
        -- but the effort to build this, combined with the low number of users of this feature, as well as most packages
        -- already using daml-script has led us to leave this as is. We'll fix this if someone is affected and notifies us.
        setupPackageDb "." opts releaseVersion [scriptDar] [] mempty
  
        opts <- pure opts
          { optPackageDbs = packageDatabasePath : optPackageDbs opts
          , optPackageImports = packageFlag : optPackageImports opts
          -- Drop version information to avoid package clashes (specifically when generating for internal packages)
          , optMbPackageVersion = Nothing
          }
  
        -- We don’t add a logger here since we will otherwise emit logging messages twice.
        importPaths <-
          if importSource
            then
              withDamlIdeState opts { optScriptService = EnableScriptService False }
                logger (NotificationHandler $ \_ _ -> pure ()) $
                \ideState -> runActionSync ideState $ do
                  pmS <- catMaybes <$> uses GetParsedModule files'
                  -- This is horrible but we do not have a way to change the import paths in a running
                  -- IdeState at the moment.
                  pure $ nubOrd $ mapMaybe (uncurry moduleImportPath) (zip files' pmS)
            else pure []
        opts <- pure opts
          { optImportPath = importPaths <> optImportPath opts
          , optHaddock = Haddock True
          }
        withDamlIdeState opts logger diagnosticsLogger $ \ideState ->
            docTest ideState files'
  
  data MultiPackageManifestEntry = MultiPackageManifestEntry
    { packageName :: String
    , packageVersion :: String
    , packageDir :: FilePath
    , packageSrc :: FilePath
    , packageFileHashes :: Map.Map FilePath String
    , packageHash :: String
    , output :: FilePath
    , packageDeps :: [String]
    , darDeps :: [FilePath]
    }
    deriving (Show, Eq, Generic, ToJSON, FromJSON)
  
  -- Map can't be a bifunctor because of this `Ord` constraint, but its a shame such a function isn't defined in Data.Map
  bimapMap :: Ord k' => (k -> k') -> (v -> v') -> Map.Map k v -> Map.Map k' v'
  bimapMap keyMap valueMap = fmap valueMap . Map.mapKeys keyMap
  
  execGenerateMultiPackageManifest :: SdkVersion.Class.SdkVersioned => MultiPackageLocation -> GenerateMultiPackageManifestOutput -> Command
  execGenerateMultiPackageManifest multiPackageLocation outputLocation =
      Command GenerateMultiPackageManifest Nothing effect
    where
      effect = do
        mMultiPackageConfigProjectPath <- getMultiPackagePath multiPackageLocation Nothing
        multiPackageConfigProjectPath <- case mMultiPackageConfigProjectPath of
          Nothing -> do
            hPutStrLn stderr "Couldn't find a multi-package.yaml at current or parent directory. Use --multi-package-path to specify its location."
            exitFailure
          Just path -> pure path
        let multiPackageConfigPath = unwrapPackagePath multiPackageConfigProjectPath
        withMultiPackageConfig multiPackageConfigProjectPath $ \multiPackageConfig -> do
          configs <- forM (mpPackagePaths multiPackageConfig) $ \path -> do
            config@BuildMultiPackageConfig {..} <- buildMultiPackageConfigFromDamlYaml path
            darPath <- deriveDarPath path bmName bmVersion bmOutput
            pure (path, darPath, config)
  
          -- Map from dar path to config, for classifying data deps
          let configMap = Map.fromList $ (\(_, darPath, config) -> (darPath, config)) <$> configs
  
          manifestEntries <- forM configs $ \(packagePath, darPath, BuildMultiPackageConfig {..}) -> do
            damlFilesMap <- bimapMap (bmSourceDaml </>) BSL.toStrict <$> getDamlFilesBuildMulti (hPutStrLn stderr) packagePath bmSourceDaml
            damlYamlContent <- B.readFile $ packagePath </> packageConfigName
  
            let manifestFileHash :: B.ByteString -> String
                manifestFileHash = show . Hash.hash @_ @Hash.SHA256
                makeRelativeToRoot :: FilePath -> FilePath
                makeRelativeToRoot path = makeRelative multiPackageConfigPath $ normalise $ packagePath </> path
                relativeDamlFilesMap = Map.mapKeys makeRelativeToRoot $ Map.insert ("." </> packageConfigName) damlYamlContent damlFilesMap
                relativeDamlFileHashes = manifestFileHash <$> relativeDamlFilesMap
                fullHash = manifestFileHash $ BSUTF8.fromString $ Map.foldMapWithKey (<>) relativeDamlFileHashes
  
            (packageDeps, darDeps) <- fmap partitionEithers $ withCurrentDirectory packagePath $ forM bmDarDeps $ \depPath -> do
              canonDepPath <- canonicalizePath depPath
              case Map.lookup canonDepPath configMap of
                Just BuildMultiPackageConfig {..} -> pure $ Left $ unitIdString $ pkgNameVersion bmName $ Just bmVersion
                Nothing -> pure $ Right $ makeRelative multiPackageConfigPath canonDepPath
  
            pure MultiPackageManifestEntry
              { packageName = T.unpack $ LF.unPackageName bmName
              , packageVersion = T.unpack $ LF.unPackageVersion bmVersion
              , packageDir = makeRelative multiPackageConfigPath packagePath
              , packageSrc = makeRelativeToRoot bmSourceDaml
              , packageFileHashes = relativeDamlFileHashes
              , packageHash = fullHash
              , output = makeRelative multiPackageConfigPath darPath
              , packageDeps = packageDeps
              , darDeps = darDeps
              }
          let encodedManifestEntries = Aeson.Pretty.encodePretty manifestEntries
          case getGenerateMultiPackageManifestOutput outputLocation of
            Nothing -> BSLC.putStrLn encodedManifestEntries
            Just path -> do
              BSL.writeFile path encodedManifestEntries
              putStrLn $ "Wrote manifest to " <> path
  
  --------------------------------------------------------------------------------
  -- main
  --------------------------------------------------------------------------------
  
  options :: SdkVersion.Class.SdkVersioned => Int -> Parser Command
  options numProcessors =
      subparser
        (  cmdIde numProcessors
        <> cmdMultiIde numProcessors
        <> cmdUpgradeCheck numProcessors
        <> cmdLicense
        <> cmdBuild numProcessors
        <> cmdTest numProcessors
        <> Damldoc.cmd numProcessors (\cli -> Command DamlDoc Nothing $ Damldoc.exec cli)
        <> cmdInspectDar
        <> cmdValidateDar
        <> cmdDocTest numProcessors
        <> cmdLint numProcessors
        <> cmdStudio
        )
      <|> subparser
        (internal -- internal commands
          <> cmdInspect
          <> cmdInit numProcessors
          <> cmdCompile numProcessors
          <> cmdDesugar numProcessors
          <> cmdDebugIdeSpanInfo numProcessors
          <> cmdClean
          <> cmdGenerateMultiPackageManifest
        )
  
  parserInfo :: SdkVersion.Class.SdkVersioned => Int -> Bool -> ParserInfo Command
  parserInfo numProcessors addBuildArgsBackupParser =
    info (backupParserWithBuildArgs addBuildArgsBackupParser $ helper <*> options numProcessors)
      (  fullDesc
      <> progDesc "Invoke the Daml compiler. Use -h for help."
      <> headerDoc (Just $ PP.vcat
          [ "damlc - Compiler and IDE backend for the Daml programming language"
          , buildInfo
          ])
      )
  
  -- | Add the build parser as a backup for when we're adding the CLI args from daml.yaml, incase whatever command
  -- we're running doesn't recognise all of the `build-options:` e.g. `daml test` cannot use `--output`
  backupParserWithBuildArgs :: SdkVersion.Class.SdkVersioned => Bool -> Parser Command -> Parser Command
  backupParserWithBuildArgs shouldBackup parser = if shouldBackup then parser <* cmdBuildParser 1 else parser
  
  -- | Attempts to find the --output flag in the given build-options for a package
  -- Given the many ways --output can be specified, we invoke the parser directly
  -- Sadly, optparse-applicative gives no way to "ignore" flags it doesn't know about
  -- so we must provide all the other flags we might expect.
  -- Note: if we ever fully remove a flag in future, an "ignore" parser will need to be added here so
  -- that old packages with that flag specified don't trigger an error.
  scrapeOutputFlag :: [String] -> Maybe FilePath
  scrapeOutputFlag args =
    case execParserPure (prefs mempty) (info mbOutFileParser mempty) args of
      Success mPath -> mPath
      Failure err -> error $ fst $ renderFailure err "daml build"
      CompletionInvoked _ -> error "Impossible internal completion invoked."
    where
      mbOutFileParser :: Parser (Maybe FilePath)
      mbOutFileParser = do
        void $ optionsParser
          0
          (EnableScriptService False)
          (pure Nothing)
          disabledDlintUsageParser
        mbOutFile <- optionalOutputFileOpt
        void incrementalBuildOpt
        void initPkgDbOpt
        pure mbOutFile
  
  -- | Query the optional `--output` flag from the daml.yaml build-options
  queryPackageConfigBuildOutput :: PackageConfig -> Either ConfigError (Maybe FilePath)
  queryPackageConfigBuildOutput package = do
    mBuildOptions <- queryPackageConfig ["build-options"] package
    pure $ mBuildOptions >>= scrapeOutputFlag
  
  -- | Calculates the canonical path to the DAR for a given package, overriding with a given mOutput if needed
  deriveDarPath :: FilePath -> LF.PackageName -> LF.PackageVersion -> Maybe FilePath -> IO FilePath
  deriveDarPath path name version mOutput = case mOutput of
    Just output -> withCurrentDirectory path $ canonicalizePath output
    Nothing -> pure $ path </> distDir </> unitIdString (pkgNameVersion name (Just version)) <> ".dar"
  
  -- | Derive the dar path from the daml.yaml, using either the --output override, or the default path derived from name and version
  darPathFromDamlYaml :: FilePath -> IO FilePath
  darPathFromDamlYaml path = do
    (name, version, mOutput) <-
      onDamlYaml 
        (\e -> error $ "Failed to parse daml.yaml at " <> path <> " for multi-package build: " <> displayException e)
        (\package -> do
          name <- queryPackageConfigRequired ["name"] package
          version <- queryPackageConfigRequired ["version"] package
          mOutput <- queryPackageConfigBuildOutput package
          pure (name, version, mOutput)
        ) mPackageOpts
    deriveDarPath path name version mOutput
    where
      mPackageOpts = Just $ PackageLocationOpts (Just $ PackagePath path) (PackageLocationCheck "" False)
  
  -- | Subset of parsePackageConfig to get only what we need for deferring to the correct build call with multi-package build
  buildMultiPackageConfigFromDamlYaml :: FilePath -> IO BuildMultiPackageConfig
  buildMultiPackageConfigFromDamlYaml path =
    onDamlYaml
      (\e -> error $ "Failed to parse daml.yaml at " <> path <> " for multi-package build: " <> displayException e)
      (\package -> do
        bmSdkVersion <- queryPackageConfigRequired ["sdk-version"] package
        bmName <- queryPackageConfigRequired ["name"] package
        bmVersion <- queryPackageConfigRequired ["version"] package
        bmSourceDaml <- queryPackageConfigRequired ["source"] package
        dataDeps <- fromMaybe [] <$> queryPackageConfig ["data-dependencies"] package
        deps <- fromMaybe [] <$> queryPackageConfig ["dependencies"] package
        upgradesDar <- maybe [] pure <$> queryPackageConfig ["upgrades"] package
        let bmDarDeps = dataDeps <> filter (\dep -> takeExtension dep == ".dar") deps <> upgradesDar
        bmOutput <- queryPackageConfigBuildOutput package
        pure $ BuildMultiPackageConfig {..}
      )
      mPackageOpts
    where
      mPackageOpts = Just $ PackageLocationOpts (Just $ PackagePath path) (PackageLocationCheck "" False)
  
  -- | Extract some value from a daml.yaml via a projection function. Return a default value if the file doesn't exist
  onDamlYaml :: (ConfigError -> t) -> (PackageConfig -> Either ConfigError t) -> Maybe PackageLocationOpts -> IO t
  onDamlYaml def f mPackageOpts = do
      -- This is the same logic used in withPackageRoot but we don’t need to change CWD here
      -- and this is simple enough so we inline it here.
      mEnvPackagePath <- fmap PackagePath <$> getPackagePath
      let mbProjectPath = packageRoot =<< mPackageOpts
      let packagePath = fromMaybe (PackagePath ".") (mbProjectPath <|> mEnvPackagePath)
      handle (\(e :: ConfigError) -> pure $ def e) $ do
          package <- readPackageConfig packagePath
          either throwIO pure $ f package
  
  cliArgsFromDamlYaml :: Maybe PackageLocationOpts -> IO [String]
  cliArgsFromDamlYaml =
    onDamlYaml (const []) $ \package -> Right $ case queryPackageConfigRequired ["build-options"] package of
      Left _ -> []
      Right xs -> xs
  
  fullParseArgs :: SdkVersion.Class.SdkVersioned => Int -> [String] -> IO Command
  fullParseArgs numProcessors cliArgs = do
      let parse :: [String] -> Maybe [String] -> ([String], ParserResult Command)
          parse args mBuildOptions =
            case mBuildOptions of
              Nothing -> ParseArgs.lax (parserInfo numProcessors False) args
              Just damlYamlArgs -> ([], execParserPure defaultPrefs (parserInfo numProcessors True) $ args ++ damlYamlArgs)
  
      let (_, tempParseResult) = parse cliArgs Nothing
      -- Note: need to parse given args first to decide whether we need to add
      -- args from daml.yaml.
      Command cmd mPackageOpts _ <- handleParseResult tempParseResult
  
      (errMsgs, parseResult) <- if cmdUseDamlYamlArgs cmd
        then
          parse cliArgs . Just <$> cliArgsFromDamlYaml mPackageOpts
        else
          pure $ parse cliArgs Nothing
  
      cmd <- handleParseResult parseResult
      forM_ errMsgs $ \msg -> do
          hPutStrLn stderr msg
  
      pure cmd
  
  warnDeprecatedArgs :: [String] -> IO ()
  warnDeprecatedArgs cliArgs =
    forM_ cliArgs $ \case
      arg | "--project-root" `isPrefixOf` arg ->
        hPutStrLn stderr "--project-root is deprecated, please use --package-root"
      arg | "--project-check" `isPrefixOf` arg ->
        hPutStrLn stderr "--project-check is deprecated, please use --package-check"
      _ -> pure ()
  
  main :: SdkVersion.Class.SdkVersioned => IO ()
  main = do
      -- We need this to ensure that logs are flushed on SIGTERM.
      installSignalHandlers
      -- Save the runfiles environment to work around
      -- https://gitlab.haskell.org/ghc/ghc/-/issues/18418.
      setRunfilesEnv
  
      numProcessors <- getNumProcessors
      cliArgs <- getArgs
      warnDeprecatedArgs cliArgs
  
      Command _ _ io <- fullParseArgs numProcessors cliArgs
  
      withProgName "damlc" io
  
      -- GHC is having some typechecking issues with `io` above, only solution I could find was adding `pure ()`
      pure ()
  
  -- | Commands for which we add the args from daml.yaml build-options.
  cmdUseDamlYamlArgs :: CommandName -> Bool
  cmdUseDamlYamlArgs = \case
    Build -> True
    Clean -> False -- don't need any flags to remove files
    Compile -> True
    DamlDoc -> True
    DebugIdeSpanInfo -> True
    Desugar -> True
    DocTest -> True
    Ide -> True
    Init -> True
    Inspect -> False -- just reads the dalf
    InspectDar -> False -- just reads the dar
    ValidateDar -> False -- just reads the dar
    License -> False -- just prints the license
    Lint -> True
    Test -> True
    GenerateMultiPackageManifest -> False -- Just reads config files
    MultiIde -> False
    UpgradeCheck -> False -- just reads the DARs it is given
    Studio -> False
  
  withPackageRoot' :: PackageLocationOpts -> ((FilePath -> IO FilePath) -> IO a) -> IO a
  withPackageRoot' PackageLocationOpts{..} act =
      withPackageRoot packageRoot packageLocationCheck (const act)
  
  addResolutionData :: Options -> IO Options
  addResolutionData opts@Options{optResolutionData = Nothing} = do
    resolutionData <- getResolutionData
    pure $ opts {optResolutionData = resolutionData}
  -- No need to reparse if its already available
  addResolutionData opts = pure opts
  