hwm 0.2.0 → 0.3.0
raw patch · 32 files changed
+813/−743 lines, 32 filesdep +http-clientdep +http-typesdep +retrydep −hspecdep −temporarydep ~Globdep ~ansi-terminaldep ~asyncsetup-changed
Dependencies added: http-client, http-types, retry
Dependencies removed: hspec, temporary
Dependency ranges changed: Glob, ansi-terminal, async, base16-bytestring, cryptohash-sha256, hpack, hwm, modern-uri, typed-process, unordered-containers, yaml, zip-archive
Files
- Setup.hs +2/−0
- hwm.cabal +155/−188
- src/HWM/CLI/Command/Registry/Add.hs +2/−4
- src/HWM/CLI/Command/Registry/Audit.hs +1/−2
- src/HWM/CLI/Command/Release/Artifacts.hs +24/−20
- src/HWM/CLI/Command/Release/Publish.hs +32/−14
- src/HWM/CLI/Command/Sync.hs +4/−4
- src/HWM/CLI/Command/Version.hs +2/−4
- src/HWM/CLI/Command/Workspace/Add.hs +11/−14
- src/HWM/Core/Formatting.hs +11/−13
- src/HWM/Core/Pkg.hs +21/−12
- src/HWM/Domain/Build.hs +116/−0
- src/HWM/Domain/Config.hs +1/−2
- src/HWM/Domain/ConfigT.hs +12/−6
- src/HWM/Domain/Dependencies.hs +31/−10
- src/HWM/Domain/Environments.hs +7/−3
- src/HWM/Domain/Workspace.hs +6/−12
- src/HWM/Integrations/Scaffold.hs +11/−4
- src/HWM/Integrations/Toolchain/Cabal.hs +122/−47
- src/HWM/Integrations/Toolchain/Hie.hs +2/−2
- src/HWM/Integrations/Toolchain/Hpack.hs +26/−21
- src/HWM/Integrations/Toolchain/Nix.hs +4/−4
- src/HWM/Integrations/Toolchain/Package.hs +56/−37
- src/HWM/Integrations/Toolchain/Stack.hs +8/−64
- src/HWM/Runtime/Files.hs +37/−12
- src/HWM/Runtime/Network.hs +98/−49
- src/HWM/Runtime/UI.hs +11/−3
- test/Commands/Init.hs +0/−21
- test/Commands/Sync.hs +0/−35
- test/Main.hs +0/−10
- test/Utils/Core.hs +0/−88
- test/Utils/Golden.hs +0/−38
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
hwm.cabal view
@@ -1,199 +1,166 @@-cabal-version: 1.12-name: hwm-version: 0.2.0-license: MIT-license-file: LICENSE-copyright: (c) 2026 Daviti Nalchevanidze-maintainer: d.nalchevanidze@gmail.com-author: Daviti Nalchevanidze-homepage: https://github.com/nalchevanidze/hwm#readme-bug-reports: https://github.com/nalchevanidze/hwm/issues-synopsis:- Haskell Workspace Manager - Orchestrates Stack, Cabal, and HLS+cabal-version: 1.12 -description:- HWM (Haskell Workspace Manager) manages multi-package Haskell projects by- generating and synchronizing configuration files for Stack, Cabal, Hpack, and HLS- from a single source of truth (hwm.yaml). It handles dependency management,- build matrices across GHC versions, and coordinated package releases.+-- This file has been generated from package.yaml by hpack version 0.36.1.+--+-- see: https://github.com/sol/hpack -category: Development-build-type: Simple+name: hwm+version: 0.3.0+synopsis: Haskell Workspace Manager - Orchestrates Stack, Cabal, and HLS+description: HWM (Haskell Workspace Manager) manages multi-package Haskell projects by+ generating and synchronizing configuration files for Stack, Cabal, Hpack, and HLS+ from a single source of truth (hwm.yaml). It handles dependency management,+ build matrices across GHC versions, and coordinated package releases.+category: Development+homepage: https://github.com/nalchevanidze/hwm#readme+bug-reports: https://github.com/nalchevanidze/hwm/issues+author: Daviti Nalchevanidze+maintainer: d.nalchevanidze@gmail.com+copyright: (c) 2026 Daviti Nalchevanidze+license: MIT+license-file: LICENSE+build-type: Simple extra-source-files: README.md CHANGELOG.md source-repository head- type: git- location: https://github.com/nalchevanidze/hwm+ type: git+ location: https://github.com/nalchevanidze/hwm library- exposed-modules:- HWM.CLI.App- HWM.CLI.Command- HWM.CLI.Command.Environment.Add- HWM.CLI.Command.Environment.Ls- HWM.CLI.Command.Environment.Remove- HWM.CLI.Command.Environment.Root- HWM.CLI.Command.Environment.SetDefault- HWM.CLI.Command.Init- HWM.CLI.Command.Registry.Add- HWM.CLI.Command.Registry.Audit- HWM.CLI.Command.Registry.Ls- HWM.CLI.Command.Registry.Root- HWM.CLI.Command.Release.Artifacts- HWM.CLI.Command.Release.Publish- HWM.CLI.Command.Release.Root- HWM.CLI.Command.Run- HWM.CLI.Command.Status- HWM.CLI.Command.Sync- HWM.CLI.Command.Version- HWM.CLI.Command.Workspace.Add- HWM.CLI.Command.Workspace.Ls- HWM.CLI.Command.Workspace.Root- HWM.Core.Common- HWM.Core.Formatting- HWM.Core.Has- HWM.Core.Options- HWM.Core.Parsing- HWM.Core.Pkg- HWM.Core.Result- HWM.Core.Version- HWM.Domain.Bounds- HWM.Domain.Config- HWM.Domain.ConfigT- HWM.Domain.Dependencies- HWM.Domain.Environments- HWM.Domain.Registry- HWM.Domain.Release- HWM.Domain.Workspace- HWM.Integrations.Scaffold- HWM.Integrations.Toolchain.Cabal- HWM.Integrations.Toolchain.Github- HWM.Integrations.Toolchain.Hie- HWM.Integrations.Toolchain.Hpack- HWM.Integrations.Toolchain.Nix- HWM.Integrations.Toolchain.Package- HWM.Integrations.Toolchain.Stack- HWM.Runtime.Archive- HWM.Runtime.Cache- HWM.Runtime.Files- HWM.Runtime.Logging- HWM.Runtime.Network- HWM.Runtime.Platform- HWM.Runtime.Process- HWM.Runtime.Snapshots- HWM.Runtime.UI-- hs-source-dirs: src- other-modules: Paths_hwm- default-language: Haskell2010- ghc-options: -Wall- build-depends:- Cabal >=3.8 && <=3.16.1.0,- Glob >=0.10.1 && <=0.10.2,- aeson >=1.5.6.0 && <=2.2.3.0,- ansi-terminal >=0.11.3 && <=1.1.5,- async >2.2.3 && <=2.2.6,- base >=4.7.0 && <5.0.0,- base16-bytestring >=1.0.1.0 && <=1.0.2.0,- bytestring >=0.10.4 && <=0.12.2.0,- containers >=0.4.2.1 && <=0.8,- cryptohash-sha256 >=0.11.102.0 && <=0.11.102.1,- directory >=1.0 && <=1.3.10.1,- filepath >=1.1.0 && <=1.5.5.0,- hpack >0.34.4 && <=0.39.1,- modern-uri >=0.3.4.1 && <=0.3.6.1,- mtl >2.0.0 && <2.6.0,- optparse-applicative >=0.16.1.0 && <=0.19.0.0,- process >=1.0.0 && <2.0.0,- relude >=0.7.0.0 && <=1.2.2.2,- req >=3.9.0 && <=3.13.4,- stm >=2.4 && <2.6.0,- text >=1.2.3 && <3.0.0,- time >=1.9.2 && <2.0.0,- transformers >=0.5.6 && <0.7.0,- typed-process >=0.2.6.1 && <=0.2.13.0,- unordered-containers >=0.2.14.0 && <=0.2.21,- yaml >=0.11.5.0 && <=0.11.11.2,- zip-archive >=0.4.1 && <=0.4.3.2+ exposed-modules:+ HWM.CLI.App+ HWM.CLI.Command+ HWM.CLI.Command.Environment.Add+ HWM.CLI.Command.Environment.Ls+ HWM.CLI.Command.Environment.Remove+ HWM.CLI.Command.Environment.Root+ HWM.CLI.Command.Environment.SetDefault+ HWM.CLI.Command.Init+ HWM.CLI.Command.Registry.Add+ HWM.CLI.Command.Registry.Audit+ HWM.CLI.Command.Registry.Ls+ HWM.CLI.Command.Registry.Root+ HWM.CLI.Command.Release.Artifacts+ HWM.CLI.Command.Release.Publish+ HWM.CLI.Command.Release.Root+ HWM.CLI.Command.Run+ HWM.CLI.Command.Status+ HWM.CLI.Command.Sync+ HWM.CLI.Command.Version+ HWM.CLI.Command.Workspace.Add+ HWM.CLI.Command.Workspace.Ls+ HWM.CLI.Command.Workspace.Root+ HWM.Core.Common+ HWM.Core.Formatting+ HWM.Core.Has+ HWM.Core.Options+ HWM.Core.Parsing+ HWM.Core.Pkg+ HWM.Core.Result+ HWM.Core.Version+ HWM.Domain.Bounds+ HWM.Domain.Build+ HWM.Domain.Config+ HWM.Domain.ConfigT+ HWM.Domain.Dependencies+ HWM.Domain.Environments+ HWM.Domain.Registry+ HWM.Domain.Release+ HWM.Domain.Workspace+ HWM.Integrations.Scaffold+ HWM.Integrations.Toolchain.Cabal+ HWM.Integrations.Toolchain.Github+ HWM.Integrations.Toolchain.Hie+ HWM.Integrations.Toolchain.Hpack+ HWM.Integrations.Toolchain.Nix+ HWM.Integrations.Toolchain.Package+ HWM.Integrations.Toolchain.Stack+ HWM.Runtime.Archive+ HWM.Runtime.Cache+ HWM.Runtime.Files+ HWM.Runtime.Logging+ HWM.Runtime.Network+ HWM.Runtime.Platform+ HWM.Runtime.Process+ HWM.Runtime.Snapshots+ HWM.Runtime.UI+ other-modules:+ Paths_hwm+ hs-source-dirs:+ src+ ghc-options: -Wall+ build-depends:+ Cabal >=3.8 && <=3.16.1.0+ , Glob >=0.10.2 && <=0.10.2+ , aeson >=1.5.6.0 && <=2.2.3.0+ , ansi-terminal >=0.11.1 && <=1.1.5+ , async >=2.2.4 && <=2.2.6+ , base >=4.7.0 && <5.0.0+ , base16-bytestring >=1.0.2.0 && <=1.0.2.0+ , bytestring >=0.10.4 && <=0.12.2.0+ , containers >=0.4.2.1 && <=0.8+ , cryptohash-sha256 >=0.11.102.1 && <=0.11.102.1+ , directory >=1.0 && <=1.3.10.1+ , filepath >=1.1.0 && <=1.5.5.0+ , hpack >=0.34.6 && <=0.39.3+ , http-client >=0.6.4.1 && <=0.7.19+ , http-types >=0.12.3.0 && <=0.12.4+ , modern-uri >=0.3.4.3 && <=0.3.6.1+ , mtl >2.0.0 && <2.6.0+ , optparse-applicative >=0.16.1.0 && <=0.19.0.0+ , process >=1.0.0 && <2.0.0+ , relude >=0.7.0.0 && <=1.2.2.2+ , req >=3.9.0 && <=3.13.4+ , retry >=0.8.1 && <=0.9.3.1+ , stm >=2.4 && <2.6.0+ , text >=1.2.3 && <3.0.0+ , time >=1.9.2 && <2.0.0+ , transformers >=0.5.6 && <0.7.0+ , typed-process >=0.2.8.0 && <=0.2.13.0+ , unordered-containers >=0.2.16.0 && <=0.2.21+ , yaml >=0.11.8.0 && <=0.11.11.2+ , zip-archive >=0.4.2.1 && <=0.4.3.2+ default-language: Haskell2010 executable hwm- main-is: Main.hs- hs-source-dirs: app- other-modules: Paths_hwm- default-language: Haskell2010- ghc-options: -Wall- build-depends:- Cabal >=3.8 && <=3.16.1.0,- Glob >=0.10.1 && <=0.10.2,- aeson >=1.5.6.0 && <=2.2.3.0,- ansi-terminal >=0.11.3 && <=1.1.5,- async >2.2.3 && <=2.2.6,- base >=4.7.0 && <5.0.0,- base16-bytestring >=1.0.1.0 && <=1.0.2.0,- bytestring >=0.10.4 && <=0.12.2.0,- containers >=0.4.2.1 && <=0.8,- cryptohash-sha256 >=0.11.102.0 && <=0.11.102.1,- directory >=1.0 && <=1.3.10.1,- filepath >=1.1.0 && <=1.5.5.0,- hpack >0.34.4 && <=0.39.1,- hwm >=0.2.0 && <0.3.0,- modern-uri >=0.3.4.1 && <=0.3.6.1,- mtl >2.0.0 && <2.6.0,- optparse-applicative >=0.16.1.0 && <=0.19.0.0,- process >=1.0.0 && <2.0.0,- relude >=0.7.0.0 && <=1.2.2.2,- req >=3.9.0 && <=3.13.4,- stm >=2.4 && <2.6.0,- text >=1.2.3 && <3.0.0,- time >=1.9.2 && <2.0.0,- transformers >=0.5.6 && <0.7.0,- typed-process >=0.2.6.1 && <=0.2.13.0,- unordered-containers >=0.2.14.0 && <=0.2.21,- yaml >=0.11.5.0 && <=0.11.11.2,- zip-archive >=0.4.1 && <=0.4.3.2--test-suite hwm-golden-sync- type: exitcode-stdio-1.0- main-is: Main.hs- hs-source-dirs: test- other-modules:- Commands.Init- Commands.Sync- Utils.Core- Utils.Golden- Paths_hwm-- default-language: Haskell2010- ghc-options: -Wall- build-depends:- Cabal >=3.8 && <=3.16.1.0,- Glob >=0.10.1 && <=0.10.2,- aeson >=1.5.6.0 && <=2.2.3.0,- ansi-terminal >=0.11.3 && <=1.1.5,- async >2.2.3 && <=2.2.6,- base >=4.7.0 && <5.0.0,- base16-bytestring >=1.0.1.0 && <=1.0.2.0,- bytestring >=0.10.4 && <=0.12.2.0,- containers >=0.4.2.1 && <=0.8,- cryptohash-sha256 >=0.11.102.0 && <=0.11.102.1,- directory >=1.0 && <=1.3.10.1,- filepath >=1.1.0 && <=1.5.5.0,- hpack >0.34.4 && <=0.39.1,- hspec >=0.0 && >=0.0,- modern-uri >=0.3.4.1 && <=0.3.6.1,- mtl >2.0.0 && <2.6.0,- optparse-applicative >=0.16.1.0 && <=0.19.0.0,- process >=1.0.0 && <2.0.0,- relude >=0.7.0.0 && <=1.2.2.2,- req >=3.9.0 && <=3.13.4,- stm >=2.4 && <2.6.0,- temporary >=0.0 && >=0.0,- text >=1.2.3 && <3.0.0,- time >=1.9.2 && <2.0.0,- transformers >=0.5.6 && <0.7.0,- typed-process >=0.2.6.1 && <=0.2.13.0,- unordered-containers >=0.2.14.0 && <=0.2.21,- yaml >=0.11.5.0 && <=0.11.11.2,- zip-archive >=0.4.1 && <=0.4.3.2+ main-is: Main.hs+ other-modules:+ Paths_hwm+ hs-source-dirs:+ app+ ghc-options: -Wall+ build-depends:+ Cabal >=3.8 && <=3.16.1.0+ , Glob >=0.10.2 && <=0.10.2+ , aeson >=1.5.6.0 && <=2.2.3.0+ , ansi-terminal >=0.11.1 && <=1.1.5+ , async >=2.2.4 && <=2.2.6+ , base >=4.7.0 && <5.0.0+ , base16-bytestring >=1.0.2.0 && <=1.0.2.0+ , bytestring >=0.10.4 && <=0.12.2.0+ , containers >=0.4.2.1 && <=0.8+ , cryptohash-sha256 >=0.11.102.1 && <=0.11.102.1+ , directory >=1.0 && <=1.3.10.1+ , filepath >=1.1.0 && <=1.5.5.0+ , hpack >=0.34.6 && <=0.39.3+ , http-client >=0.6.4.1 && <=0.7.19+ , http-types >=0.12.3.0 && <=0.12.4+ , hwm >=0.3.0 && <0.4.0+ , modern-uri >=0.3.4.3 && <=0.3.6.1+ , mtl >2.0.0 && <2.6.0+ , optparse-applicative >=0.16.1.0 && <=0.19.0.0+ , process >=1.0.0 && <2.0.0+ , relude >=0.7.0.0 && <=1.2.2.2+ , req >=3.9.0 && <=3.13.4+ , retry >=0.8.1 && <=0.9.3.1+ , stm >=2.4 && <2.6.0+ , text >=1.2.3 && <3.0.0+ , time >=1.9.2 && <2.0.0+ , transformers >=0.5.6 && <0.7.0+ , typed-process >=0.2.8.0 && <=0.2.13.0+ , unordered-containers >=0.2.16.0 && <=0.2.21+ , yaml >=0.11.8.0 && <=0.11.11.2+ , zip-archive >=0.4.2.1 && <=0.4.3.2+ default-language: Haskell2010
src/HWM/CLI/Command/Registry/Add.hs view
@@ -16,7 +16,7 @@ import HWM.Domain.Registry (addDependency, lookupBounds) import HWM.Domain.Workspace (forWorkspaceTuple, resolveWorkspaces) import HWM.Integrations.Toolchain.Package-import HWM.Runtime.UI (putLine, section, sectionConfig, sectionTableM)+import HWM.Runtime.UI (putLine, section, sectionTableM) import Options.Applicative (argument, help, long, metavar, short, str) import Relude @@ -46,9 +46,7 @@ bounds <- deriveBounds opsPkgName range let dependency = Dependency opsPkgName bounds - ((\cf -> pure cf {cfgRegistry = Just $ addDependency dependency (fromMaybe mempty (cfgRegistry cf))}) `updateConfig`) $ do- sectionConfig [("hwm.yaml", pure $ chalk Green "✓")]- addDepToPackage workspaces dependency+ ((\cf -> pure cf {cfgRegistry = Just $ addDependency dependency (fromMaybe mempty (cfgRegistry cf))}) `updateConfig`) $ addDepToPackage workspaces dependency Just bounds -> do section "discovery" $ do putLine $ padDots 16 "registry" <> format bounds <> " (already registered)"
src/HWM/CLI/Command/Registry/Audit.hs view
@@ -13,7 +13,7 @@ import HWM.Domain.Environments (getTestedRange) import HWM.Domain.Registry (askRegistry, mapDeps, mapWithName) import HWM.Integrations.Toolchain.Package (syncPackages)-import HWM.Runtime.UI (indent, printGenTable, putLine, section, sectionConfig, sectionTableM)+import HWM.Runtime.UI (indent, printGenTable, putLine, section, sectionTableM) import Options.Applicative import Relude @@ -41,7 +41,6 @@ else do if auditFix then ((\cf -> pure $ cf {cfgRegistry = Just $ mapDeps (updateDepBounds auditForce range) originalRegistry}) `updateConfig`) $ do- sectionConfig [("hwm.yaml", pure $ chalk Green "✓")] syncPackages else do injectIssue
src/HWM/CLI/Command/Release/Artifacts.hs view
@@ -1,10 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE NoImplicitPrelude #-}-{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} -{-# HLINT ignore "Redundant $" #-}- module HWM.CLI.Command.Release.Artifacts ( ReleaseArchiveOptions (..), parseCLI,@@ -20,12 +17,13 @@ import HWM.Core.Parsing (Parse (..), ParseCLI (..), parseLS) import HWM.Core.Pkg (Pkg (..)) import HWM.Core.Result (fromEither)+import HWM.Domain.Build (Builder (..), buildBinary) import HWM.Domain.Config (Config (..)) import HWM.Domain.ConfigT (ConfigT, Env (..), getArchiveConfigs)+import HWM.Domain.Environments (BuildEnvironment (..), getBuildEnvironment) import HWM.Domain.Release (ArchiveFormat, ArtifactConfig (..), ReleaseArtifactConfigs, selectedArtifacts) import HWM.Domain.Workspace (resolveWorkspaces) import HWM.Integrations.Toolchain.Github (ensureIsLatestTag)-import HWM.Integrations.Toolchain.Stack (stackGenBinary) import HWM.Runtime.Archive (ArchiveInfo (..), ArchivingPlan (..), createArchive) import HWM.Runtime.Network (getGHUploadUrl, uploadToGitHub) import HWM.Runtime.UI (forTable, indent, putLine, section, sectionTableM)@@ -41,7 +39,8 @@ outputDir :: FilePath, ovFormat :: Maybe [Text], ovGhcOptions :: Maybe [Text],- ovNameTemplate :: Maybe Text+ ovNameTemplate :: Maybe Text,+ opsBuilder :: Maybe Builder } deriving (Show) @@ -60,6 +59,7 @@ <*> optional (option (parseLS <$> str) (long "format" <> metavar "FORMAT" <> help "Override the archive format for the release target. Supported: zip, tar.gz.")) <*> optional (option (parseLS <$> str) (long "ghc-options" <> metavar "GHC_OPTION" <> help "Override GHC options for the release target. Can be specified multiple times.")) <*> optional (strOption (long "name-template" <> metavar "NAME_TEMPLATE" <> help "Override the name template for the release target. Use {name} and {version} as placeholders."))+ <*> optional (option (str >>= parse) (long "builder" <> metavar "BUILDER" <> help "Override the builder for the release target. Supported: cabal, stack, nix.")) genBindaryDir :: (MonadIO m, ToString a) => a -> m FilePath genBindaryDir name = do@@ -102,14 +102,17 @@ cfgs <- getArchiveConfigs >>= withOverrides ops ghTag <- if ghPublish then Just <$> ensureIsLatestTag version else pure Nothing uploadUrl <- maybe (pure Nothing) (fmap Just . getGHUploadUrl cfg) ghTag-- sectionTableM "artifacts"- $ [ ("destination", pure $ maybe (format outputDir) format uploadUrl),- ("version", pure $ format version <> maybe "" (\tag -> " (GitHub Release " <> tag <> ")") ghTag),- ("targets", pure $ formatList "," (map fst cfgs))- ]+ defaultBuilder <- buildBuilder <$> getBuildEnvironment Nothing+ let builder = fromMaybe defaultBuilder opsBuilder+ sectionTableM+ "artifacts"+ [ ("destination", pure $ maybe (format outputDir) format uploadUrl),+ ("version", pure $ format version <> maybe "" (\tag -> " (GitHub Release " <> tag <> ")") ghTag),+ ("targets", pure $ formatList "," (map fst cfgs)),+ ("builder", pure $ format builder)+ ] - plans <- forTable "build" cfgs (\x -> (fst x, buildPkg x))+ plans <- forTable "build" cfgs (\x -> (fst x, buildPkg outputDir builder x)) section "archive" $ pure () artifacts <- for plans $ \(name, plan) -> do@@ -129,11 +132,12 @@ putLine $ subPathSign <> format archivePath uploadToGitHub url sha256Path putLine $ subPathSign <> format sha256Path- where- buildPkg (name, ArtifactConfig {..}) = do- binaryDir <- genBindaryDir name- let (workspaceId, executableName) = second (T.drop 1) (T.breakOn ":" arcSource)- optTarget <- listToMaybe . concatMap snd <$> resolveWorkspaces [workspaceId]- Pkg {..} <- maybe (throwError $ fromString $ toString $ "Package \"" <> workspaceId <> "\" not found in any workspace. Check package name and workspace configuration.") pure optTarget- stackGenBinary pkgName binaryDir (ghcOptions arcGhcOptions)- pure $ (statusIcon Checked, ArchivingPlan {nameTemplate = arcNameTemplate, outDir = outputDir, sourceDir = binaryDir, name = executableName, archiveFormats = arcFormats})++buildPkg :: FilePath -> Builder -> (Name, ArtifactConfig) -> ConfigT (Text, ArchivingPlan)+buildPkg outputDir builder (name, ArtifactConfig {..}) = do+ binaryDir <- genBindaryDir name+ let (workspaceId, executableName) = second (T.drop 1) (T.breakOn ":" arcSource)+ optTarget <- listToMaybe . concatMap snd <$> resolveWorkspaces [workspaceId]+ Pkg {..} <- maybe (throwError $ fromString $ toString $ "Package \"" <> workspaceId <> "\" not found in any workspace. Check package name and workspace configuration.") pure optTarget+ buildBinary builder pkgName binaryDir (ghcOptions arcGhcOptions)+ pure (statusIcon Checked, ArchivingPlan {nameTemplate = arcNameTemplate, outDir = outputDir, sourceDir = binaryDir, name = executableName, archiveFormats = arcFormats})
src/HWM/CLI/Command/Release/Publish.hs view
@@ -16,6 +16,7 @@ import HWM.Core.Formatting ( Color (..), Format (..),+ Status (Checked, Invalid), chalk, genMaxLen, padDots,@@ -23,24 +24,33 @@ ) import HWM.Core.Parsing (ParseCLI (..)) import HWM.Core.Pkg (Pkg (..))-import HWM.Core.Result (Issue, Severity (..), maxSeverity)+import HWM.Core.Result (Issue (..), MonadIssue (..), Severity (..), maxSeverity) import HWM.Domain.Config (Config (cfgRelease)) import HWM.Domain.ConfigT (ConfigT, Env (..), askVersion) import HWM.Domain.Dependencies (sortByDependencyHierarchy) import HWM.Domain.Release (Release (..)) import HWM.Domain.Workspace (WsPkgs, printPkgWSRef, resolveWsPkgs)+import HWM.Integrations.Toolchain.Cabal (nativeSdist) import HWM.Integrations.Toolchain.Package (deriveDependencyGraph)-import HWM.Integrations.Toolchain.Stack (sdist, upload)+import HWM.Runtime.Network (getHackageToken, uploadToHackage) import HWM.Runtime.UI (printSummary, putLine, section, sectionTableM) import Options.Applicative (argument, help, metavar, str) import Relude hiding (intercalate)+import System.Directory (getCurrentDirectory)+import System.FilePath (makeRelative) failIssues :: [Issue] -> ConfigT () failIssues [] = pure ()-failIssues issues = do- printSummary issues- when (maxSeverity issues == Just SeverityError) $ liftIO exitFailure+failIssues issues+ | maxSeverity issues == Just SeverityError = do+ printSummary issues+ liftIO exitFailure+ | otherwise = traverse_ injectIssue issues +unpackPath :: (Pkg, Maybe FilePath) -> ConfigT (Pkg, FilePath)+unpackPath (pkg, Just path) = pure (pkg, path)+unpackPath (pkg, Nothing) = throwError $ fromString $ "No file path found for package " <> toString (printPkgWSRef pkg)+ newtype PublishOptions = PublishOptions { publishGroup :: Maybe Name }@@ -79,16 +89,24 @@ ] pkgs <- arrangePackageRelease (concatMap snd wgs)- let size = genMaxLen (map printPkgWSRef pkgs)- section "publishing plan (topological sort)" $ do- for_ (zip pkgs [1 ..] :: [(Pkg, Int)]) $ \(pkg, idx) -> do- putLine $ "└── " <> padDots size (printPkgWSRef pkg) <> show idx - issues <- traverse sdist (concatMap snd wgs)- failIssues (concat issues)+ sdists <- traverse nativeSdist pkgs+ failIssues (concatMap snd sdists)+ releasePkgs <- traverse (unpackPath . fst) sdists+ cwd <- liftIO getCurrentDirectory + let ls = zip releasePkgs [1 ..] :: [((Pkg, FilePath), Int)]++ let size = genMaxLen (map (\((pkg, _), n) -> show n <> ". " <> printPkgWSRef pkg) ls)++ section "publishing plan (topological sort)" $ do+ for_ ls $ \((pkg, filePath), idx) -> do+ putLine $ "└── " <> padDots size (show idx <> ". " <> printPkgWSRef pkg) <> fromString (makeRelative cwd filePath)++ token <- getHackageToken section "publishing" $ do- for_ pkgs $ \pkg -> do- (status, publishIssues) <- upload pkg+ for_ releasePkgs $ \(pkg, filePath) -> do+ issues <- uploadToHackage token pkg filePath+ let status = if null issues then Checked else Invalid putLine $ "└── " <> padDots size (printPkgWSRef pkg) <> statusIcon status- failIssues publishIssues+ failIssues issues
src/HWM/CLI/Command/Sync.hs view
@@ -26,9 +26,9 @@ ("resolver", pure $ buildResolver env) ] sectionConfig- ( [("cabal.project", syncCabalProject $> chalk Green "✓")]- <> [("stack.yaml", syncStackYaml $> chalk Green "✓") | buildStack env]- <> [("flake.nix", syncNixFile $> chalk Green "✓") | buildNix env]- <> [("hie.yaml", syncHie $> chalk Green "✓")]+ ( [("cabal.project", syncCabalProject)]+ <> [("stack.yaml", syncStackYaml) | buildStack env]+ <> [("flake.nix", syncNixFile) | buildNix env]+ <> [("hie.yaml", syncHie)] ) syncPackages
src/HWM/CLI/Command/Version.hs view
@@ -15,7 +15,7 @@ import HWM.Domain.Config (Config (..)) import HWM.Domain.ConfigT (ConfigT, config, updateConfig) import HWM.Integrations.Toolchain.Package (syncPackages)-import HWM.Runtime.UI (putLine, sectionConfig, sectionTableM)+import HWM.Runtime.UI (putLine, sectionTableM) import Options.Applicative (argument, help, metavar) import Options.Applicative.Builder (str) import Relude@@ -54,9 +54,7 @@ pure Config {cfgVersion = version', ..} runVersion :: VersionOptions -> ConfigT ()-runVersion (VersionOptions (Just bump)) = (bumpVersion bump `updateConfig`) $ do- sectionConfig [("hwm.yaml", pure $ chalk Green "✓")]- syncPackages+runVersion (VersionOptions (Just bump)) = bumpVersion bump `updateConfig` syncPackages runVersion (VersionOptions Nothing) = do putLine . format . cfgVersion =<< asks config exitSuccess
src/HWM/CLI/Command/Workspace/Add.hs view
@@ -6,17 +6,17 @@ import Control.Monad.Error.Class (MonadError (throwError)) import qualified Data.Map as Map-import HWM.Core.Formatting (Color (..), Status (Checked), chalk, displayStatus, padDots, subPathSign)+import HWM.Core.Formatting (Color (..), Status (Checked), chalk, displayStatusM, padDots, subPathSign) import HWM.Core.Parsing (ParseCLI (..)) import HWM.Core.Pkg (PkgName (..), mkPkgDirPath, resolvePrefix) import HWM.Core.Result (Issue (..), MonadIssue (injectIssue), Severity (SeverityError, SeverityWarning)) import HWM.Domain.Config (Config (..))-import HWM.Domain.ConfigT (ConfigT, Env (config), updateConfig)+import HWM.Domain.ConfigT (ConfigT, Env (config), updateConfig, updateConfigM) import HWM.Domain.Workspace (WorkGroup (..), WorkspaceRef (..), addWorkgroupMember, parseWorkspaceRef) import HWM.Integrations.Scaffold (scaffoldPackage) import HWM.Integrations.Toolchain.Hie (syncHie) import HWM.Integrations.Toolchain.Stack (syncStackYaml)-import HWM.Runtime.UI (putLine, sectionConfig, sectionWorkspace)+import HWM.Runtime.UI (putLine, sectionWorkspace) import Options.Applicative (help, long, metavar, strArgument, strOption) import Relude @@ -47,25 +47,22 @@ } else do let ws = Map.insert groupId (WorkGroup opsWorkspaceDir [] opsPrefix) wss- updateConfig (\cfg -> pure $ cfg {cfgWorkspace = ws}) $ sectionWorkspace $ do+ sectionWorkspace $ do putLine ""- displayStatus [("added", pure Checked)] >>= putLine . (("• " <> chalk Bold groupId <> " ") <>)+ displayStatusM [("added", pure Checked)] >>= putLine . (("• " <> chalk Bold groupId <> " ") <>)+ updateConfig (\cfg -> pure $ cfg {cfgWorkspace = ws}) (pure ()) runWorkspaceAdd (WorkspaceAddOptions {opsWorkspaceId = WorkspaceRef groupId (Just memberId), ..}) = do when (isJust opsPrefix) $ injectIssue (noEffect "prefix") when (isJust opsWorkspaceDir) $ injectIssue (noEffect "dir") (ws, w) <- addWorkgroupMember groupId memberId- scaffoldPackage (mkPkgDirPath (dir w) (prefix w) memberId) (PkgName $ resolvePrefix (prefix w) memberId)- updateConfig (\cfg -> pure $ cfg {cfgWorkspace = ws})- $ sectionWorkspace+ sectionWorkspace $ do putLine "" putLine $ "• " <> chalk Bold groupId- displayStatus [("added", pure Checked)] >>= putLine . ((subPathSign <> padDots 16 memberId) <>)- sectionConfig- [ ("stack.yaml", syncStackYaml $> chalk Green "✓"),- ("hie.yaml", syncHie $> chalk Green "✓")- ]- pure ()+ xs <- scaffoldPackage (mkPkgDirPath (dir w) (prefix w) memberId) (PkgName $ resolvePrefix (prefix w) memberId)+ displayStatusM xs >>= putLine . ((subPathSign <> padDots 16 memberId) <>)++ updateConfigM (\cfg -> pure $ cfg {cfgWorkspace = ws}) [("stack.yaml", syncStackYaml), ("hie.yaml", syncHie)] $ pure () where noEffect label = Issue
src/HWM/Core/Formatting.hs view
@@ -1,10 +1,7 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NoImplicitPrelude #-}-{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} -{-# HLINT ignore "Eta reduce" #-}- module HWM.Core.Formatting ( Color (..), chalk,@@ -16,8 +13,7 @@ deriveStatus, statusFromSeverity, statusIcon,- isOk,- displayStatus,+ displayStatusM, Format (..), availableOptions, renderSummaryStatus,@@ -32,6 +28,7 @@ formatTemplate, toCamelCase, formatTableRow,+ StatusM, ) where @@ -42,6 +39,7 @@ import Data.Text (pack) import qualified Data.Text as T import Distribution.PackageDescription (UnqualComponentName, unUnqualComponentName)+import HWM.Core.Common (Name) import HWM.Core.Result (MonadIssue (catchIssues), Severity (..)) import Relude @@ -87,9 +85,14 @@ chalk :: Color -> Text -> Text chalk c x = toColor c <> x <> toColor None +type StatusM m = [(Name, m Status)]+ data Status = Checked | Updated | Warning | Invalid deriving (Show, Eq, Ord) +instance Semigroup Status where+ a <> b = max a b+ deriveStatus :: [Status] -> Status deriveStatus [] = Checked deriveStatus statuses = maximum statuses@@ -103,11 +106,11 @@ monadStatus :: (Functor m, MonadIssue m) => m b -> m Status monadStatus x = statusFromSeverity . fst <$> catchIssues x -displayStatus :: (Monad m) => [(Text, m Status)] -> m Text-displayStatus ls = do+displayStatusM :: (Monad m) => StatusM m -> m Text+displayStatusM ls = do statuses <- mapM snd ls let status = deriveStatus statuses- if isOk status then return (statusIcon status) else return (formatStatus (zip (map fst ls) statuses))+ if status == Checked then return (statusIcon status) else return (formatStatus (zip (map fst ls) statuses)) formatTemplate :: [(Text, Text)] -> Text -> Text formatTemplate vars template =@@ -118,11 +121,6 @@ padDots :: Int -> Text -> Text padDots width s = s <> " " <> chalk Dim (T.replicate (max 0 (width - T.length s)) ".") <> " "--isOk :: Status -> Bool-isOk Checked = True-isOk Updated = True-isOk _ = False labelColor :: Status -> Color labelColor Checked = Dim
src/HWM/Core/Pkg.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GeneralisedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE NoImplicitPrelude #-}@@ -17,7 +18,8 @@ PkgSource (..), cabalSource, hpackSource,- checkVersion,+ getVersionIssues,+ PackageIO (..), ) where @@ -35,7 +37,7 @@ import HWM.Core.Formatting import HWM.Core.Has (Has) import HWM.Core.Parsing (Parse (..))-import HWM.Core.Result (Issue (..), IssueDetails (..), MonadIssue (..), Severity (..))+import HWM.Core.Result (Issue (..), IssueDetails (..), Severity (..)) import HWM.Core.Version (Version, askVersion, fromCabalVersion, toCabalVersion) import HWM.Runtime.Files (cleanRelativePath) import Relude hiding (Undefined, intercalate)@@ -74,6 +76,10 @@ getPkgVersion :: a -> Version setVersion :: Version -> a -> a +class PackageIO a m where+ rewritePackage :: (a -> m (Maybe a)) -> Pkg -> m Status+ readPackage :: Pkg -> m a+ instance IsPkg GenericPackageDescription where getPkgName = PkgName . toText . unPackageName . packageName . package . packageDescription getPkgVersion = fromCabalVersion . pkgVersion . package . packageDescription@@ -138,15 +144,18 @@ instance Parse PkgName where parse = pure . PkgName -checkVersion :: (IsPkg a, MonadReader env m, Has env Version, MonadIssue m) => PkgSource -> a -> m ()-checkVersion source pkg = do+getVersionIssues :: (MonadReader env m, Has env Version, IsPkg p) => PkgSource -> p -> m [Issue]+getVersionIssues source pkg = do expectedVersion <- askVersion let version = getPkgVersion pkg- unless (version == expectedVersion)- $ injectIssue- Issue- { issueTopic = pkgSourceName source,- issueMessage = "version mismatch: " <> format version <> " → " <> format expectedVersion,- issueSeverity = SeverityWarning,- issueDetails = Just GenericIssue {issueFile = pkgSourceFile source}- }+ if version == expectedVersion+ then pure []+ else+ pure+ [ Issue+ { issueTopic = pkgSourceName source,+ issueMessage = "version mismatch: " <> format version <> " → " <> format expectedVersion,+ issueSeverity = SeverityWarning,+ issueDetails = Just GenericIssue {issueFile = pkgSourceFile source}+ }+ ]
+ src/HWM/Domain/Build.hs view
@@ -0,0 +1,116 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE NoImplicitPrelude #-}++module HWM.Domain.Build+ ( Builder (..),+ buildBinary,+ )+where++import Control.Monad.Error.Class (MonadError)+import Control.Monad.Except (throwError)+import Data.Aeson (FromJSON (..), ToJSON (toJSON))+import Data.Aeson.Types (Value (..))+import HWM.Core.Formatting (Format (..))+import HWM.Core.Parsing (Parse (..))+import HWM.Core.Pkg (PkgName)+import HWM.Core.Result (Issue)+import HWM.Runtime.Process (exec)+import Relude+import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist, doesPathExist, emptyPermissions, removeFile, setOwnerExecutable, setOwnerReadable, setOwnerWritable, setPermissions)+import System.FilePath ((</>))++data Builder+ = CabalBuilder+ | StackBuilder+ | NixBuilder+ deriving (Generic, Show, Ord, Eq)++instance FromJSON Builder where+ parseJSON (String s) = parse s+ parseJSON _ = fail "Invalid builder. Expected 'cabal', 'stack', or 'nix'."++instance Parse Builder where+ parse "cabal" = pure CabalBuilder+ parse "stack" = pure StackBuilder+ parse "nix" = pure NixBuilder+ parse _ = fail "Invalid builder. Expected 'cabal', 'stack', or 'nix'."++instance ToJSON Builder where+ toJSON = String . format++instance Format Builder where+ format CabalBuilder = "cabal"+ format StackBuilder = "stack"+ format NixBuilder = "nix"++buildBinary :: (MonadError Issue m, MonadIO m) => Builder -> PkgName -> FilePath -> [Text] -> m ()+buildBinary builder pkgName dirPath args = do+ (success, buildOut) <- command+ unless success $ throwError (fromString $ "Build failed: " <> buildOut)+ when (builder == NixBuilder) (extractNixArtifact pkgName dirPath)+ where+ command = case builder of+ StackBuilder ->+ exec "stack" $ ["install", format pkgName, "--local-bin-path", format dirPath] <> args+ CabalBuilder ->+ exec "cabal"+ $ [ "install",+ format pkgName,+ "--install-method=copy",+ "--installdir",+ format dirPath,+ "--overwrite-policy=always"+ ]+ <> args+ NixBuilder ->+ -- WARNING: We DO NOT append 'args' here.+ -- Nix does not accept '--ghc-options' via CLI; it must be set in the flake.+ exec "nix" ["build", ".#" <> format pkgName, "-o", format (dirPath </> "result")]++extractNixArtifact :: (MonadIO m, MonadError Issue m) => PkgName -> FilePath -> m ()+extractNixArtifact pkgName distDir = do+ let resultLink = distDir </> "result"+ finalDest = distDir </> toString pkgName+ pkgStr = toString (format pkgName)++ liftIO $ createDirectoryIfMissing True distDir+ isLink <- liftIO $ doesPathExist resultLink+ unless isLink+ $ throwError+ $ fromString+ $ "Nix build completed, but did not create an output at: "+ <> resultLink+ <> "\n(This usually means the Nix derivation is empty or 'exec' hid a build failure.)"+ let searchPaths =+ [ resultLink </> "bin" </> pkgStr, -- Standard Haskell (Cabal/Stack)+ resultLink </> pkgStr, -- Simple/Single-binary derivation+ resultLink -- Derivation is the binary itself+ ]+ maybeSource <- findM (liftIO . doesFileExist) searchPaths++ case maybeSource of+ Just sourcePath -> do+ liftIO $ copyFile sourcePath finalDest+ -- Ensure the user can execute it (Nix store is read-only)+ liftIO $ do+ let properPerms =+ setOwnerReadable True+ $ setOwnerWritable True+ $ setOwnerExecutable True emptyPermissions+ setPermissions finalDest properPerms+ -- Cleanup: Remove the 'result' symlink to keep the folder clean+ liftIO $ removeFile resultLink+ Nothing ->+ throwError+ $ fromString+ $ "Nix build succeeded, but binary '"+ <> pkgStr+ <> "' not found inside the Nix store path.\n"++findM :: (Monad m) => (a -> m Bool) -> [a] -> m (Maybe a)+findM _ [] = pure Nothing+findM p (x : xs) = do+ ifM (p x) (pure $ Just x) (findM p xs)
src/HWM/Domain/Config.hs view
@@ -87,6 +87,5 @@ [ ("build", "stack build --fast"), ("test", "stack test {TARGET} --fast"), ("install", "stack install"),- ("lint", "curl -sSL https://raw.github.com/ndmitchell/hlint/master/misc/run.sh | sh -s ."),- ("clean", "find . -name \"*.cabal\" -exec rm -rf {} \\; && stack clean")+ ("lint", "curl -sSL https://raw.github.com/ndmitchell/hlint/master/misc/run.sh | sh -s .") ]
src/HWM/Domain/ConfigT.hs view
@@ -15,6 +15,7 @@ runConfigT, VersionMap, updateConfig,+ updateConfigM, Env (..), unpackConfigT, askCache,@@ -28,7 +29,7 @@ import Control.Monad.Error.Class import HWM.Core.Common (Check (..), Name)-import HWM.Core.Formatting (Format (..))+import HWM.Core.Formatting (Format (..), Status (..), StatusM) import HWM.Core.Has (Has (..)) import HWM.Core.Options (Options (..)) import HWM.Core.Result (Issue (..), MonadIssue (..), Result (..), ResultT, runResultT)@@ -40,7 +41,7 @@ import HWM.Domain.Workspace (PkgRegistry, Workspace, pkgRegistry) import HWM.Runtime.Cache (Cache, VersionMap, loadCache, saveCache) import HWM.Runtime.Files (Signature, addHash, getFileSignature, readYaml, rewrite_)-import HWM.Runtime.UI (MonadUI (..), UIT, printSummary, runUI)+import HWM.Runtime.UI (MonadUI (..), UIT, printSummary, runUI, sectionConfig) import Relude data Env (m :: Type -> Type) = Env@@ -118,13 +119,18 @@ saveConfig :: (MonadError Issue m, MonadIO m) => Config -> Options -> m () saveConfig config ops = do let file = optionsHwm ops- rewrite_ file (const $ pure config)- addHash file (environmentHash (cfgEnvironments config))+ status <- rewrite_ file (const $ pure config)+ case status of+ Updated -> addHash file (environmentHash (cfgEnvironments config))+ _ -> pure () updateConfig :: (Config -> ConfigT Config) -> ConfigT b -> ConfigT b-updateConfig f m = do+updateConfig f = updateConfigM f []++updateConfigM :: (Config -> ConfigT Config) -> StatusM ConfigT -> ConfigT b -> ConfigT b+updateConfigM f xs m = do config' <- asks config >>= f- local (\e -> e {config = config'}) (checkConfig >> m)+ local (\e -> e {config = config'}) (checkConfig >> sectionConfig (("hwm.yaml", pure Updated) : xs) >> m) runConfigT :: ConfigT () -> Options -> IO () runConfigT m opts@Options {..} = do
src/HWM/Domain/Dependencies.hs view
@@ -19,10 +19,11 @@ collectNormalizedDependencies, buildDependencyGraph, MapDeps (..),- DependencyIssue,+ DependencyBoundsIssue (..), reportDependencyIssues, detectDependencyIssue, mkCabalDependency,+ collectDependencyIssues, ) where @@ -30,7 +31,9 @@ import Data.Aeson ( FromJSON (..), ToJSON (..),+ Value (..), )+import Data.Foldable (maximum) import qualified Data.Map.Strict as Map import qualified Data.Set as Set import qualified Data.Text as T@@ -43,7 +46,7 @@ import Distribution.Version (LowerBound (..), VersionInterval (..)) import qualified Distribution.Version as Cabal import HWM.Core.Common (Name)-import HWM.Core.Formatting (Format (..), formatTable, subPathSign)+import HWM.Core.Formatting (Format (..), formatTableRow, subPathSign) import HWM.Core.Parsing (Parse (..), firstWord) import HWM.Core.Pkg (IsPkg (..), Pkg (..), PkgName (..), PkgSource (..)) import HWM.Core.Result (Issue (..), IssueDetails (..), MonadIssue (..), Severity (..))@@ -83,10 +86,17 @@ toDependencyList (Dependencies m) = map (uncurry Dependency) $ Map.toList m instance FromJSON Dependencies where- parseJSON v = initDependencies <$> (parseJSON v >>= traverse parse . sort)+ parseJSON (Array v) = initDependencies <$> (parseJSON (Array v) >>= traverse parse . sort)+ parseJSON v = Dependencies <$> parseJSON v instance ToJSON Dependencies where- toJSON = toJSON . formatTable . map format . toDependencyList+ toJSON (Dependencies ms) = toJSON . Map.mapWithKey formatTable $ ms+ where+ formatTable key value =+ let padding = T.replicate (size - T.length (format key)) " "+ in String (padding <> formatTableRow table (T.words (format value)))+ size = maximum $ map (T.length . format) $ Map.keys ms+ table = map (T.words . format) $ Map.elems ms fromDependencyList :: [Dependency] -> Dependencies fromDependencyList = initDependencies@@ -352,9 +362,20 @@ toRange (Bound Min inc version) = if inc then Cabal.orLaterVersion (toCabalVersion version) else Cabal.laterVersion (toCabalVersion version) toRange (Bound Max inc version) = if inc then Cabal.orEarlierVersion (toCabalVersion version) else Cabal.earlierVersion (toCabalVersion version) -type DependencyIssue = (Text, PkgName, Bounds, Maybe Bounds)+data DependencyBoundsIssue = DependencyBoundsIssue+ { depIssueScope :: Text,+ depIssueName :: PkgName,+ depIssueActual :: Bounds,+ depIssueRegistryBounds :: Maybe Bounds+ } -reportDependencyIssues :: (MonadIssue m, Applicative m) => PkgSource -> [DependencyIssue] -> m ()+collectDependencyIssues :: (Monad m, HasDependencies a) => (PkgName -> m (Maybe Bounds)) -> a -> m [DependencyBoundsIssue]+collectDependencyIssues f pkg = concat <$> traverse checkForDependencyIssues (Map.toList (Map.fromList (collectDependencies [] pkg)))+ where+ checkForDependencyIssues (path, deps) = concat <$> traverse (getIssue path) (toDependencyList deps)+ getIssue path dep = detectDependencyIssue path dep <$> f (hwmDepName dep)++reportDependencyIssues :: (MonadIssue m, Applicative m) => PkgSource -> [DependencyBoundsIssue] -> m () reportDependencyIssues source diffs = do unless (null diffs) $ injectIssue@@ -365,15 +386,15 @@ issueDetails = Just DependencyIssue- { issueDependencies = map (\(scope, dName, actual, expected) -> (scope, format dName, format actual, maybe "unknown" format expected)) diffs,+ { issueDependencies = map (\(DependencyBoundsIssue scope dName actual expected) -> (scope, format dName, format actual, maybe "unknown" format expected)) diffs, issueFile = pkgSourceFile source } } -detectDependencyIssue :: [Text] -> Dependency -> Maybe Bounds -> [DependencyIssue]+detectDependencyIssue :: [Text] -> Dependency -> Maybe Bounds -> [DependencyBoundsIssue] detectDependencyIssue path (Dependency dname depBounds) registryBounds = case registryBounds of- Nothing -> [(scope, dname, depBounds, Nothing)]- Just expected -> ([(scope, dname, depBounds, Just expected) | depBounds /= expected])+ Nothing -> [DependencyBoundsIssue scope dname depBounds Nothing]+ Just expected -> [DependencyBoundsIssue scope dname depBounds (Just expected) | depBounds /= expected] where scope = T.intercalate ":" path
src/HWM/Domain/Environments.hs view
@@ -46,6 +46,7 @@ import HWM.Core.Result (Issue) import HWM.Core.Version (Era (..), Version, selectEra) import HWM.Domain.Bounds (TestedRange (..))+import HWM.Domain.Build (Builder (..)) import HWM.Domain.Workspace (Workspace, WorkspaceRef, allPackages, checkWorkspaceRefs, isMember) import HWM.Runtime.Cache (Cache, Registry (currentEnv), VersionMap, getLatestNightlySnapshot, getRegistry, getSnapshot, getVersions) import HWM.Runtime.Files (Signature, aesonYAMLOptions, aesonYAMLOptionsAdvanced, genSignature)@@ -55,7 +56,8 @@ type Extras = VersionMap data Environments = Environments- { envDefault :: Name,+ { envBuilder :: Maybe Builder,+ envDefault :: Name, envNix :: Maybe Bool, envStack :: Maybe Bool, envProfiles :: Map Name Enviroment@@ -184,7 +186,8 @@ buildResolver :: Name, buildAllowNewer :: Maybe Bool, buildStack :: Bool,- buildNix :: Bool+ buildNix :: Bool,+ buildBuilder :: Builder } deriving ( Generic,@@ -218,7 +221,8 @@ buildGHC = ghc env, buildAllowNewer = stack env >>= unfeature >>= allowNewer, buildStack = isEnabled (envStack globalEnv) (stack env),- buildNix = isEnabled (envNix globalEnv) (nix env)+ buildNix = isEnabled (envNix globalEnv) (nix env),+ buildBuilder = fromMaybe CabalBuilder (envBuilder globalEnv) } where excludePkgs build pkgs =
src/HWM/Domain/Workspace.hs view
@@ -18,7 +18,6 @@ forWorkspace, forWorkspaceTuple, parseWorkspaceRef,- forWorkspaceCore, addWorkgroupMember, allPackages, resolveWsPkgs,@@ -46,7 +45,7 @@ import qualified Data.Set as S import qualified Data.Text as T import HWM.Core.Common (Name)-import HWM.Core.Formatting (Color (..), availableOptions, chalk, commonPrefix, genMaxLen, monadStatus, padDots, slugify, statusIcon, subPathSign)+import HWM.Core.Formatting (Color (..), StatusM, availableOptions, chalk, commonPrefix, displayStatusM, genMaxLen, padDots, slugify, subPathSign) import HWM.Core.Has (Has (..), askEnv) import HWM.Core.Pkg (Pkg (..), PkgName (..), makePkg) import HWM.Core.Result@@ -192,13 +191,8 @@ compareMembers _ "." = LT compareMembers x y = compare x y -forWorkspace :: (MonadIO m, MonadUI m, MonadIssue m, MonadError Issue m, MonadReader env m, Has env Workspace) => (Pkg -> m ()) -> m ()-forWorkspace f = forWorkspaceCore $ \pkg -> do- status <- monadStatus (f pkg)- pure $ statusIcon status--forWorkspaceCore :: (MonadIO m, MonadUI m, MonadIssue m, MonadError Issue m, MonadReader env m, Has env Workspace) => (Pkg -> m Text) -> m ()-forWorkspaceCore f = do+forWorkspace :: (MonadIO m, MonadUI m, MonadIssue m, MonadError Issue m, MonadReader env m, Has env Workspace) => (Pkg -> StatusM m) -> m ()+forWorkspace f = do gs <- Map.toList <$> askWorkspace sectionWorkspace $ for_ gs@@ -208,17 +202,17 @@ pkgs <- memberPkgs (name, wg) let maxLen = genMaxLen (map pkgMemberId pkgs) for_ pkgs $ \pkg -> do- status <- f pkg+ status <- displayStatusM (f pkg) putLine $ subPathSign <> padDots maxLen (pkgMemberId pkg) <> status -forWorkspaceTuple :: (MonadUI m) => [(Text, [Pkg])] -> (Pkg -> m Text) -> m ()+forWorkspaceTuple :: (MonadUI m) => [(Text, [Pkg])] -> (Pkg -> StatusM m) -> m () forWorkspaceTuple ws f = sectionWorkspace $ do let maxLen = genMaxLen (map pkgMemberId $ concatMap snd ws) for_ ws $ \(name, pkgs) -> do putLine "" putLine $ "• " <> chalk Bold name for_ pkgs $ \pkg -> do- status <- f pkg+ status <- displayStatusM (f pkg) putLine (subPathSign <> padDots maxLen (pkgMemberId pkg) <> status) addWorkgroupMember :: (MonadIO m, MonadUI m, MonadIssue m, MonadError Issue m, MonadReader env m, Has env Workspace) => Name -> Name -> m (Workspace, WorkGroup)
src/HWM/Integrations/Scaffold.hs view
@@ -4,6 +4,7 @@ module HWM.Integrations.Scaffold (scaffoldPackage) where import qualified Data.Text as T+import HWM.Core.Formatting (Status (..), StatusM) import HWM.Core.Pkg (PkgName) import HWM.Domain.ConfigT (ConfigT) import HWM.Integrations.Toolchain.Package (newPackage)@@ -20,8 +21,14 @@ "someFunc = putStrLn \"Scaffolded by HWM\"" ] -scaffoldPackage :: FilePath -> PkgName -> ConfigT ()+writeSource :: FilePath -> ConfigT (StatusM ConfigT)+writeSource targetDir = liftIO $ do+ createDirectoryIfMissing True (targetDir </> "src")+ writeFile (targetDir </> "src/Lib.hs") (T.unpack libHsTemplate)+ pure [("src", pure Updated)]++scaffoldPackage :: FilePath -> PkgName -> ConfigT (StatusM ConfigT) scaffoldPackage targetDir pkgName = do- liftIO $ createDirectoryIfMissing True (targetDir </> "src")- newPackage targetDir pkgName- liftIO $ writeFile (targetDir </> "src/Lib.hs") (T.unpack libHsTemplate)+ sts <- writeSource targetDir+ fs <- map (second pure) <$> newPackage targetDir pkgName+ pure $ sts <> fs
src/HWM/Integrations/Toolchain/Cabal.hs view
@@ -1,54 +1,66 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE NoImplicitPrelude #-} module HWM.Integrations.Toolchain.Cabal- ( rewriteCabalPackage,- validateHackage,+ ( validateHackage, syncCabalProject,- readCabalPackage, HasSourceDirs (..), CabalPackage, newCabalPackage,+ readCabalPackage,+ nativeSdist, ) where +import Control.Exception (IOException)+import Control.Exception.Base (try) import Control.Monad.Except (MonadError (throwError)) import qualified Data.ByteString as BS import Data.Foldable (Foldable (..)) import qualified Data.Map as Map import qualified Data.Text as T-import qualified Data.Text.IO as TIO+import Distribution.Package (packageVersion) import Distribution.PackageDescription (Benchmark (..), Executable (..), GenericPackageDescription (..), PackageDescription (..), PackageIdentifier (..), TestSuite (..), UnqualComponentName, emptyBuildInfo, emptyLibrary, emptyPackageDescription, mkPackageName, packageDescription) import Distribution.PackageDescription.Check (PackageCheck (..), checkPackage)+import Distribution.PackageDescription.Configuration (flattenPackageDescription) import Distribution.PackageDescription.Parsec import Distribution.PackageDescription.PrettyPrint (writeGenericPackageDescription)+import Distribution.Simple.Flag (toFlag) import Distribution.Simple.PackageDescription (readGenericPackageDescription)+import Distribution.Simple.PreProcess (knownSuffixHandlers)+import Distribution.Simple.Setup (SDistFlags (..), defaultSDistFlags)+import Distribution.Simple.SrcDist (sdist)+import Distribution.Text (display) import Distribution.Types.BuildInfo (BuildInfo (..)) import Distribution.Types.CondTree (CondTree (..)) import Distribution.Types.Library (Library (..)) import Distribution.Utils.Path (getSymbolicPath, unsafeMakeSymbolicPath) import Distribution.Verbosity (normal)+import qualified Distribution.Verbosity as Verbosity import HWM.Core.Common (Name) import HWM.Core.Formatting (Format (..), Status (..)) import HWM.Core.Options (Options (..))-import HWM.Core.Pkg (IsPkg (..), Pkg (Pkg, hpackFile), PkgName)+import HWM.Core.Pkg (IsPkg (..), PackageIO, Pkg (Pkg, hpackFile), PkgName) import qualified HWM.Core.Pkg as P-import HWM.Core.Result (Issue (..), IssueDetails (..), MonadIssue (..), Severity (..))+import HWM.Core.Result (Issue (..), MonadIssue (..), Severity (..)) import HWM.Core.Version (Version, toCabalVersion) import HWM.Domain.ConfigT (ConfigT) import qualified HWM.Domain.ConfigT as CT import HWM.Domain.Dependencies (Dependencies (..), HasDependencies (..), MapDeps (..), mkCabalDependency, toDependencyList) import HWM.Domain.Environments (BuildEnvironment (..), getBuildEnvironment)-import Hpack (Result (..), defaultOptions, hpackResult, setProgramName, setTarget)+import HWM.Runtime.Files (syncFile)+import Hpack (Force (..), Options (..), Result (..), defaultOptions, hpackResult, setProgramName, setTarget) import qualified Hpack as H import Hpack.Config (ProgramName (..)) import Relude+import System.Directory (createDirectoryIfMissing, doesFileExist, makeAbsolute, removePathForcibly, renameFile, withCurrentDirectory) import System.FilePath (takeDirectory, (</>)) --- | Translate Cabal warnings into formatting status for downstream reporting. toStatus :: PackageCheck -> Status toStatus p | isError p = Invalid@@ -61,40 +73,64 @@ isError PackageDistSuspiciousWarn {} = False isError PackageDistSuspicious {} = False -validateHackage :: Pkg -> FilePath -> ConfigT [Status]-validateHackage pkg path = do- gpd <- liftIO $ readGenericPackageDescription normal path+toIssue :: Pkg -> PackageCheck -> Issue+toIssue pkg check =+ Issue+ { issueMessage = "Invalid Package [Cabal check]: " <> show check,+ issueSeverity = if isError check then SeverityError else SeverityWarning,+ issueTopic = P.pkgMemberId pkg,+ issueDetails = Nothing+ }++validateHackage :: Pkg -> ConfigT Status+validateHackage pkg = do+ gpd <- liftIO $ readGenericPackageDescription normal (P.cabalFile pkg) let ls = checkPackage gpd Nothing- for_ ls $ \l -> do- injectIssue- ( Issue- { issueMessage = "Invalid package: " <> show l,- issueSeverity = if isError l then SeverityError else SeverityWarning,- issueTopic = P.pkgMemberId pkg,- issueDetails = Just GenericIssue {issueFile = path}- }- )- pure (map toStatus ls)+ for_ ls $ \l -> injectIssue (toIssue pkg l)+ pure (maximum (Checked : map toStatus ls)) -hpackSync :: Pkg -> ConfigT Status-hpackSync Pkg {hpackFile = Nothing} = pure Checked-hpackSync pkg@Pkg {hpackFile = Just path} = do+instance PackageIO CabalPackage ConfigT where+ rewritePackage = rewriteCabalPackage+ readPackage = readCabalPackage++getChanges :: Pkg -> (CabalPackage -> ConfigT (Maybe CabalPackage)) -> ConfigT (Maybe CabalPackage)+getChanges pkg mapCabal = do+ original <- readCabalPackage pkg+ updated <- mapCabal original+ case updated of+ Nothing -> pure Nothing+ Just newpackage ->+ if cbContent newpackage == cbContent original+ then pure Nothing+ else pure (Just newpackage)++forChanges :: (Applicative f) => Maybe t -> (t -> f a) -> f Status+forChanges changes f = do+ case changes of+ Nothing -> pure Checked+ Just newpackage -> f newpackage $> Updated++rewriteCabalPackage :: (CabalPackage -> ConfigT (Maybe CabalPackage)) -> Pkg -> ConfigT Status+rewriteCabalPackage mapCabal pkg@Pkg {hpackFile = Just path} = do+ changes <- getChanges pkg mapCabal+ update <- forChanges changes $ \_ -> hpackForceUpdate pkg path+ validation <- validateHackage pkg+ pure $ max validation update+rewriteCabalPackage mapCabal pkg = do+ changes <- getChanges pkg mapCabal+ update <- forChanges changes $ \package -> liftIO $ writeGenericPackageDescription (P.cabalFile pkg) (cbContent package)+ validation <- validateHackage pkg+ pure $ max validation update++hpackForceUpdate :: (MonadIO m) => Pkg -> FilePath -> m Status+hpackForceUpdate pkg path = do let programName = ProgramName $ toString $ P.pkgName pkg- let ops = setTarget path $ setProgramName programName defaultOptions+ let ops = setTarget path $ setProgramName programName defaultOptions {optionsForce = Force} Result {..} <- liftIO $ hpackResult ops case resultStatus of H.OutputUnchanged -> pure Checked _ -> pure Updated -rewriteCabalPackage :: (CabalPackage -> ConfigT CabalPackage) -> Pkg -> ConfigT Status-rewriteCabalPackage mapCabal pkg = do- s <- hpackSync pkg- ls <- validateHackage pkg (P.cabalFile pkg)- cabalP <- readCabalPackage pkg- newpackage <- mapCabal cabalP- liftIO $ writeGenericPackageDescription (P.cabalFile pkg) (cbOriginal newpackage)- pure $ maximum (s : ls)- generateCabalProject :: [Pkg] -> Text -> Text generateCabalProject packagePaths ghcVersion = T.unlines@@ -102,15 +138,15 @@ "packages:\n" <> T.unlines (map ((" " <>) . format . P.pkgDirPath) packagePaths) ] -syncCabalProject :: ConfigT ()+syncCabalProject :: ConfigT Status syncCabalProject = do- ops <- asks CT.options+ cabalFilePath <- asks (optionsCabal . CT.options) BuildEnvironment {..} <- getBuildEnvironment Nothing- liftIO $ TIO.writeFile (optionsCabal ops) (generateCabalProject buildPkgs (toText buildGHC))+ syncFile cabalFilePath (generateCabalProject buildPkgs (toText buildGHC)) data CabalPackage = CabalPackage { cbDirectory :: FilePath,- cbOriginal :: GenericPackageDescription+ cbContent :: GenericPackageDescription } deriving (Show) @@ -129,7 +165,7 @@ pure CabalPackage { cbDirectory = takeDirectory (P.cabalFile pkg),- cbOriginal = gpd+ cbContent = gpd } class HasSourceDirs a where@@ -143,7 +179,7 @@ getSourceDirs tags libs = concatMap (\(name, lib) -> getSourceDirs (tags <> [name]) lib) (Map.toList libs) instance HasSourceDirs CabalPackage where- getSourceDirs p CabalPackage {..} = getSourceDirs p cbOriginal+ getSourceDirs p CabalPackage {..} = getSourceDirs p cbContent instance HasSourceDirs GenericPackageDescription where getSourceDirs p GenericPackageDescription {..} =@@ -176,22 +212,23 @@ withKey dir = (T.intercalate ":" path, format dir) instance IsPkg CabalPackage where- getPkgName = getPkgName . cbOriginal- getPkgVersion = getPkgVersion . cbOriginal- setVersion version pkg = pkg {cbOriginal = setVersion version (cbOriginal pkg)}+ getPkgName = getPkgName . cbContent+ getPkgVersion = getPkgVersion . cbContent+ setVersion version pkg = pkg {cbContent = setVersion version (cbContent pkg)} instance HasDependencies CabalPackage where- collectDependencies xs gpd = collectDependencies xs (cbOriginal gpd)+ collectDependencies xs gpd = collectDependencies xs (cbContent gpd) instance MapDeps CabalPackage where mapDeps ctx f cabalPkg = do- newGpd <- mapDeps ctx f (cbOriginal cabalPkg)- pure cabalPkg {cbOriginal = newGpd}+ newGpd <- mapDeps ctx f (cbContent cabalPkg)+ pure cabalPkg {cbContent = newGpd} -newCabalPackage :: (MonadError Issue m, MonadIO m) => FilePath -> PkgName -> Version -> Dependencies -> m ()+newCabalPackage :: (MonadError Issue m, MonadIO m) => FilePath -> PkgName -> Version -> Dependencies -> m Status newCabalPackage dir name version deps = do let package = emptyPackage name version deps liftIO $ writeGenericPackageDescription (dir </> (toString name <> ".cabal")) package+ pure Updated emptyPackage :: PkgName -> Version -> Dependencies -> GenericPackageDescription emptyPackage (P.PkgName name) version dependencies =@@ -218,3 +255,41 @@ condSubLibraries = [], condForeignLibs = [] }++err :: Pkg -> Text -> Issue+err pkg msg = Issue (P.pkgMemberId pkg) SeverityError msg Nothing++nativeSdist :: Pkg -> ConfigT ((Pkg, Maybe FilePath), [Issue])+nativeSdist pkg = do+ gpkg <- readCabalFile pkg+ outDir <- liftIO (makeAbsolute $ "./.hwm/sdist" </> toString (P.pkgName pkg))+ (filePath, issues) <- runNativeSDist pkg (flattenPackageDescription gpkg) outDir+ pure ((pkg, filePath), issues <> map (toIssue pkg) (checkPackage gpkg Nothing))++runNativeSDist :: Pkg -> PackageDescription -> FilePath -> ConfigT (Maybe FilePath, [Issue])+runNativeSDist pkg pkgDesc outDir = do+ let tarName = toString (P.pkgName pkg) <> "-" <> display (packageVersion (package pkgDesc)) <> ".tar.gz"+ -- We'll look for it here: package_dir/dist/package-version.tar.gz+ localDistDir = P.pkgDirPath pkg </> "dist"+ tempTarPath = localDistDir </> tarName+ finalPath = outDir </> tarName++ result <- liftIO $ try $ do+ -- Setup the HWM output dir+ removePathForcibly outDir+ createDirectoryIfMissing True outDir+ -- prepare the 'dist' folder inside the package dir and run sdist+ removePathForcibly localDistDir+ createDirectoryIfMissing True localDistDir+ withCurrentDirectory (P.pkgDirPath pkg) $ sdist pkgDesc (defaultSDistFlags {sDistVerbosity = toFlag Verbosity.silent}) (const "") knownSuffixHandlers+ exists <- doesFileExist tempTarPath+ if exists+ then renameFile tempTarPath finalPath >> pure (Just finalPath)+ else pure Nothing++ case result of+ Right (Just path) -> pure (Just path, [])+ Right Nothing ->+ pure (Nothing, [err pkg $ "Cabal sdist ran, but " <> toText tarName <> " was not found."])+ Left (e :: IOException) ->+ pure (Nothing, [err pkg $ "Cabal sdist IO Error: " <> show e])
src/HWM/Integrations/Toolchain/Hie.hs view
@@ -15,7 +15,7 @@ import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), object) import HWM.Core.Common (Name)-import HWM.Core.Formatting (Format (format))+import HWM.Core.Formatting (Format (format), Status (..)) import HWM.Core.Options (Options (..), askOptions) import HWM.Core.Pkg (Pkg (..), pkgFile) import HWM.Domain.ConfigT (ConfigT)@@ -54,7 +54,7 @@ where comp (tag, sourceDirs) = Component {path = "./" <> pkgFile pkg (toString sourceDirs), component = tag} -syncHie :: ConfigT ()+syncHie :: ConfigT Status syncHie = do Options {..} <- askOptions pkgs <- allPackages
src/HWM/Integrations/Toolchain/Hpack.hs view
@@ -1,14 +1,14 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE NoImplicitPrelude #-} module HWM.Integrations.Toolchain.Hpack ( HpackPackage (..),- readHpackPackage,- rewriteHpackPackage, newHpackPackage,+ readHpackPackage, ) where @@ -21,11 +21,12 @@ import GHC.Generics (Generic (..)) import HWM.Core.Common (Name) import HWM.Core.Formatting (Status (Checked))-import HWM.Core.Pkg (IsPkg (..), Pkg (..), PkgName (..))+import HWM.Core.Pkg (IsPkg (..), PackageIO (..), Pkg (..), PkgName (..)) import HWM.Core.Result (Issue (..), IssueDetails (..), Severity (..)) import HWM.Core.Version (Version)+import HWM.Domain.ConfigT (ConfigT) import HWM.Domain.Dependencies (Dependencies, HasDependencies (..), MapDeps (..))-import HWM.Runtime.Files (aesonYAMLOptions, aesonYAMLOptionsAdvanced, readYaml, rewrite_, statusM)+import HWM.Runtime.Files (aesonYAMLOptions, aesonYAMLOptionsAdvanced, readYaml, rewriteMaybe_, rewrite_, statusM) import Hpack () import Relude import System.FilePath ((</>))@@ -137,23 +138,9 @@ hpackForeignLibraries = Nothing } -readHpackPackage :: (Monad m, MonadError Issue m, MonadIO m) => Pkg -> m HpackPackage-readHpackPackage pkg =- maybe- ( throwError- $ Issue- { issueTopic = pkgMemberId pkg,- issueMessage = "pkg does not support hpack or could not find package file",- issueSeverity = SeverityWarning,- issueDetails = Just GenericIssue {issueFile = cabalFile pkg}- }- )- readYaml- (hpackFile pkg)--rewriteHpackPackage :: (MonadIO m, MonadError Issue m) => (HpackPackage -> m HpackPackage) -> Pkg -> m Status+rewriteHpackPackage :: (MonadIO m, MonadError Issue m) => (HpackPackage -> m (Maybe HpackPackage)) -> Pkg -> m Status rewriteHpackPackage f pkg = do- maybe (pure Checked) (\path -> statusM path (rewrite_ path maybePackage)) (hpackFile pkg)+ maybe (pure Checked) (\path -> statusM path (rewriteMaybe_ path maybePackage)) (hpackFile pkg) where maybePackage Nothing = throwError@@ -165,7 +152,25 @@ } maybePackage (Just package) = f package -newHpackPackage :: (MonadError Issue m, MonadIO m) => FilePath -> PkgName -> Version -> Dependencies -> m ()+newHpackPackage :: (MonadError Issue m, MonadIO m) => FilePath -> PkgName -> Version -> Dependencies -> m Status newHpackPackage dir name version deps = do let package = emptyPackage name version deps rewrite_ (dir </> "package.yaml") (const $ pure package)++instance PackageIO HpackPackage ConfigT where+ rewritePackage = rewriteHpackPackage+ readPackage = readHpackPackage++readHpackPackage :: (MonadError Issue m, MonadIO m) => Pkg -> m HpackPackage+readHpackPackage pkg =+ maybe+ ( throwError+ $ Issue+ { issueTopic = pkgMemberId pkg,+ issueMessage = "pkg does not support hpack or could not find package file",+ issueSeverity = SeverityWarning,+ issueDetails = Just GenericIssue {issueFile = cabalFile pkg}+ }+ )+ readYaml+ (hpackFile pkg)
src/HWM/Integrations/Toolchain/Nix.hs view
@@ -6,23 +6,23 @@ module HWM.Integrations.Toolchain.Nix (syncNixFile) where import qualified Data.Text as T-import qualified Data.Text.IO as TIO import HWM.Core.Common (Name)-import HWM.Core.Formatting (format, toCamelCase)+import HWM.Core.Formatting (Status, format, toCamelCase) import HWM.Core.Options (Options (..)) import HWM.Core.Pkg (Pkg (..)) import HWM.Core.Version (Era (eraNixpkgs), Version, formatNixGhc, selectEra) import HWM.Domain.Config (Config (Config, cfgName)) import HWM.Domain.ConfigT (ConfigT, Env (..)) import HWM.Domain.Environments (BuildEnvironment (..), getBuildEnvironment)+import HWM.Runtime.Files (syncFile) import Relude -syncNixFile :: ConfigT ()+syncNixFile :: ConfigT Status syncNixFile = do Config {..} <- asks config ops <- asks options BuildEnvironment {buildPkgs, buildGHC} <- getBuildEnvironment Nothing- liftIO $ TIO.writeFile (optionsNix ops) (deriveFlakeNix cfgName buildGHC buildPkgs)+ syncFile (optionsNix ops) (deriveFlakeNix cfgName buildGHC buildPkgs) renderNixName :: Text -> Text renderNixName name = toCamelCase name <> "WorkspacePackages"
src/HWM/Integrations/Toolchain/Package.hs view
@@ -3,7 +3,6 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-}-{-# LANGUAGE TupleSections #-} {-# LANGUAGE NoImplicitPrelude #-} module HWM.Integrations.Toolchain.Package@@ -16,46 +15,54 @@ where import qualified Data.Text as T-import HWM.Core.Formatting (displayStatus)-import HWM.Core.Pkg (IsPkg (..), Pkg (..), PkgName (PkgName), PkgSource (..), cabalSource, checkVersion, hpackSource)+import HWM.Core.Formatting (Status, StatusM, monadStatus)+import HWM.Core.Pkg (IsPkg (..), PackageIO (..), Pkg (..), PkgName (PkgName), PkgSource (..), cabalSource, getVersionIssues, hpackSource)+import HWM.Core.Result (Issue, MonadIssue (injectIssue)) import HWM.Domain.Bounds (Bounds (Bounds)) import HWM.Domain.Config (getRegistryBounds) import HWM.Domain.ConfigT (ConfigT, askVersion) import HWM.Domain.Dependencies ( Dependencies, Dependency (..),+ DependencyBoundsIssue (..), DependencyMap (..), HasDependencies (..), MapDeps (..), buildDependencyGraph,- detectDependencyIssue,+ collectDependencyIssues, fromDependencyList, reportDependencyIssues, singleDeps, toDependencyList, ) import qualified HWM.Domain.Dependencies as M-import HWM.Domain.Workspace (allPackages, forWorkspace, forWorkspaceCore)-import HWM.Integrations.Toolchain.Cabal (CabalPackage, newCabalPackage, readCabalPackage, rewriteCabalPackage)-import HWM.Integrations.Toolchain.Hpack (HpackPackage, newHpackPackage, readHpackPackage, rewriteHpackPackage)+import HWM.Domain.Workspace (allPackages, forWorkspace)+import HWM.Integrations.Toolchain.Cabal (CabalPackage, newCabalPackage, readCabalPackage, validateHackage)+import HWM.Integrations.Toolchain.Hpack (HpackPackage, newHpackPackage, readHpackPackage) import Relude -newPackage :: FilePath -> PkgName -> ConfigT ()+newPackage :: FilePath -> PkgName -> ConfigT [(Text, Status)] newPackage targetDir name = do let baseName = PkgName "base" version <- askVersion base <- fromMaybe (Bounds Nothing Nothing) <$> getRegistryBounds baseName let deps = M.singleDeps (Dependency baseName base)- newHpackPackage targetDir name version deps- newCabalPackage targetDir name version deps+ hpack <- newHpackPackage targetDir name version deps+ cabal <- newCabalPackage targetDir name version deps+ pure [("hpack", hpack), ("cabal", cabal)] syncPackages :: ConfigT ()-syncPackages = forWorkspaceCore $ updatePackage syncPackage syncPackage+syncPackages = forWorkspace $ updatePackage syncPackage syncPackage -syncPackage :: (MapDeps a, IsPkg a) => PkgSource -> a -> ConfigT a+syncPackage :: (MapDeps a, IsPkg a, HasDependencies a) => PkgSource -> a -> ConfigT (Maybe a) syncPackage pkg package = do- result <- mapDeps (pkg, []) syncDeps package- (`setVersion` result) <$> askVersion+ noIssues <- hasNoIssues pkg package+ if noIssues+ then pure Nothing+ else+ Just <$> do+ result <- mapDeps (pkg, []) syncDeps package+ (`setVersion` result) <$> askVersion syncDeps :: (PkgSource, [Text]) -> Dependencies -> ConfigT Dependencies syncDeps (pkg, path) deps =@@ -65,41 +72,53 @@ where syncDep (Dependency depName depBounds) = do bounds <- getRegistryBounds depName- pure ([(T.intercalate ":" path, depName, depBounds, Nothing) | isNothing bounds], Dependency depName (fromMaybe depBounds bounds))+ pure ([DependencyBoundsIssue (T.intercalate ":" path) depName depBounds Nothing | isNothing bounds], Dependency depName (fromMaybe depBounds bounds)) -addDeps :: (MapDeps a) => Dependency -> PkgSource -> a -> ConfigT a-addDeps dependency pkg = mapDeps (pkg, []) onlyMain+addDeps :: (MapDeps a) => Dependency -> PkgSource -> a -> ConfigT (Maybe a)+addDeps dependency pkg = fmap Just . mapDeps (pkg, []) onlyMain where onlyMain (_, ["dependencies"]) deps = pure (deps <> singleDeps dependency) onlyMain _ deps = pure deps -addPkgDependency :: Dependency -> Pkg -> ConfigT Text+addPkgDependency :: Dependency -> Pkg -> StatusM ConfigT addPkgDependency dep = updatePackage (addDeps dep) (addDeps dep) -updatePackage :: (PkgSource -> HpackPackage -> ConfigT HpackPackage) -> (PkgSource -> CabalPackage -> ConfigT CabalPackage) -> Pkg -> ConfigT Text-updatePackage mapHpack mapCabal pkg =- displayStatus- ( map (\s -> ("hpack", rewriteHpackPackage (mapHpack s) pkg)) (maybeToList $ hpackSource pkg)- <> [("cabal", rewriteCabalPackage (mapCabal (cabalSource pkg)) pkg)]- )+forFormats :: (IsString a) => (PkgSource -> Pkg -> b) -> (PkgSource -> Pkg -> b) -> Pkg -> [(a, b)]+forFormats hpack cabal pkg =+ map (\s -> ("hpack", hpack s pkg)) (maybeToList $ hpackSource pkg)+ <> [("cabal", cabal (cabalSource pkg) pkg)] +updatePackage :: (PkgSource -> HpackPackage -> ConfigT (Maybe HpackPackage)) -> (PkgSource -> CabalPackage -> ConfigT (Maybe CabalPackage)) -> Pkg -> StatusM ConfigT+updatePackage mapHpack mapCabal = forFormats (rewritePackage . mapHpack) (rewritePackage . mapCabal)+ deriveDependencyGraph :: ConfigT DependencyMap deriveDependencyGraph = buildDependencyGraph (concatMap (toDependencyList . snd) . libDependencies) <$> (allPackages >>= traverse readCabalPackage) where libDependencies = filter (\x -> fst x == ["library"]) . collectDependencies [] validatePackages :: ConfigT ()-validatePackages = forWorkspace $ \pkg -> do- cabal <- readCabalPackage pkg- hpack <- traverse (\x -> (x,) <$> readHpackPackage pkg) (maybeToList $ hpackSource pkg)- validatePackage (cabalSource pkg, cabal)- for_ hpack validatePackage--validatePackage :: (IsPkg a, HasDependencies a) => (PkgSource, a) -> ConfigT ()-validatePackage (source, package) = do- checkVersion source package- diffs <- concat <$> traverse checkForDependencyIssues (collectDependencies [] package)- reportDependencyIssues source diffs+validatePackages = forWorkspace $ forFormats hpack cabal where- checkForDependencyIssues (path, deps) = concat <$> traverse (getIssue path) (toDependencyList deps)- getIssue path dep = detectDependencyIssue path dep <$> getRegistryBounds (hwmDepName dep)+ hpack src pkg = readHpackPackage pkg >>= validatePackage src+ cabal _ pkg = do+ s1 <- readCabalPackage pkg >>= validatePackage (cabalSource pkg)+ s2 <- validateHackage pkg+ pure (s1 <> s2)++hasNoIssues :: (IsPkg a, HasDependencies a) => PkgSource -> a -> ConfigT Bool+hasNoIssues source pkg = do+ (issues, depIssues) <- collectIssues source pkg+ let depIssues' = filter (isJust . depIssueRegistryBounds) depIssues+ pure (null issues && null depIssues')++collectIssues :: (IsPkg a, HasDependencies a) => PkgSource -> a -> ConfigT ([Issue], [DependencyBoundsIssue])+collectIssues source pkg = do+ versionIssues <- getVersionIssues source pkg+ depIssues <- collectDependencyIssues getRegistryBounds pkg+ pure (versionIssues, depIssues)++validatePackage :: (IsPkg a, HasDependencies a) => PkgSource -> a -> ConfigT Status+validatePackage source package = monadStatus $ do+ (issues, depIssues) <- collectIssues source package+ traverse_ injectIssue issues+ reportDependencyIssues source depIssues
src/HWM/Integrations/Toolchain/Stack.hs view
@@ -2,7 +2,6 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE NoImplicitPrelude #-}@@ -12,13 +11,9 @@ syncStackYaml, createEnvYaml, stackPath,- sdist,- upload, parseExtraDeps, scanStackFiles, buildMatrix,- runStack,- stackGenBinary, ) where @@ -30,22 +25,20 @@ genericToJSON, ) import qualified Data.Map as Map-import Data.Text (pack) import qualified Data.Text as T import HWM.Core.Common (Name)-import HWM.Core.Formatting (Format (..), Status (..), indentBlockNum, slugify)+import HWM.Core.Formatting (Format (..), Status (..), slugify) import HWM.Core.Options (Options (..), askOptions) import HWM.Core.Parsing (Parse (..)) import HWM.Core.Pkg (Pkg (..), PkgName)-import HWM.Core.Result (Issue (..), IssueDetails (..), Severity (..), fromEither)+import HWM.Core.Result (Issue (..), fromEither) import HWM.Core.Version (Version, latestGHCVersion, parseGHCVersion)+import HWM.Domain.Build (Builder (StackBuilder)) import HWM.Domain.ConfigT (ConfigT) import HWM.Domain.Environments (BuildEnvironment (..), Enviroment (..), Environments (..), Feature (..), StackEnvironment (..), getBuildEnvironment, hkgRefs) import HWM.Domain.Workspace (toWorkspaceRef) import HWM.Runtime.Cache (getSnapshotGHC) import HWM.Runtime.Files (aesonYAMLOptions, readYaml, rewrite_)-import HWM.Runtime.Logging (logIssue)-import HWM.Runtime.Process (exec) import Relude hiding (head, tail) import System.Directory (createDirectoryIfMissing, doesFileExist, makeAbsolute) import System.FilePath (dropExtension, (</>))@@ -89,7 +82,7 @@ version <- fromEither ("Invalid version: " <> versionPart) (parse versionPart) pure (pkgName, version) -syncStackYaml :: ConfigT ()+syncStackYaml :: ConfigT Status syncStackYaml = do stackYamlPath <- optionsStack <$> askOptions rewrite_ stackYamlPath $ const $ do@@ -115,7 +108,7 @@ createEnvYaml target = do path <- stackPath (Just target) liftIO $ createDirectoryIfMissing True ".hwm/matrix/"- rewrite_ path $ const $ do+ _ <- rewrite_ path $ const $ do BuildEnvironment {..} <- getBuildEnvironment Nothing pure Stack@@ -127,56 +120,7 @@ allowNewer = buildAllowNewer, .. }--stackGenBinary :: PkgName -> FilePath -> [Text] -> ConfigT ()-stackGenBinary pkgName dirPath args = do- (success, buildOut) <- runStack (["install", format pkgName, "--local-bin-path", format dirPath] <> args)- unless success $ throwError (fromString $ "Build failed: " <> buildOut)--runStack :: [Text] -> ConfigT (Bool, String)-runStack = exec "stack"--sdist :: Pkg -> ConfigT [Issue]-sdist pkg = do- let issueTopic = pkgMemberId pkg- issueMessage = "stack sdist detected Issues. No packages were published."- (isSuccess, out) <- runStack ["sdist", format (pkgName pkg)]- let severity = if isSuccess then findIssue out else Just SeverityError- case severity of- Nothing -> pure []- Just issueSeverity -> do- issueFile <- logIssue "sdist" issueSeverity [("COMMAND", "stack sdist " <> format (pkgName pkg))] (pack out)- let issueDetails = Just GenericIssue {issueFile}- in pure [Issue {..}]--upload :: Pkg -> ConfigT (Status, [Issue])-upload pkg = do- (isSuccess, out) <- runStack ["upload", format (pkgName pkg)]- ( if isSuccess- then pure (Checked, [])- else- ( do- pure- ( Invalid,- [ Issue- { issueTopic = pkgMemberId pkg,- issueMessage = "Package publishing failed:" <> indentBlockNum 4 ("\n\n" <> T.pack out),- issueSeverity = SeverityError,- issueDetails = Just GenericIssue {issueFile = fromMaybe (cabalFile pkg) (hpackFile pkg)}- }- ]- )- )- )--findIssue :: String -> Maybe Severity-findIssue str =- let ls = map T.strip $ T.lines $ T.toLower $ T.pack str- in case find ("error:" `T.isInfixOf`) ls of- Just _ -> Just SeverityError- Nothing -> case find ("warning:" `T.isInfixOf`) ls of- Just _ -> Just SeverityWarning- Nothing -> Nothing+ pure () scanStackFiles :: (MonadIO m, MonadError Issue m) => Options -> FilePath -> m [(Name, Stack)] scanStackFiles opts root = do@@ -196,10 +140,10 @@ buildMatrix :: (MonadIO m, MonadError Issue m) => [Pkg] -> [(Name, Stack)] -> m Environments buildMatrix pkgs (defaultEnv : envs) = do environments <- sortOn (ghc . snd) <$> traverse (inferBuildEnv pkgs) (defaultEnv : envs)- pure Environments {envDefault = fst defaultEnv, envProfiles = Map.fromList environments, envStack = Just True, envNix = Nothing}+ pure Environments {envDefault = fst defaultEnv, envProfiles = Map.fromList environments, envStack = Just True, envNix = Nothing, envBuilder = Just StackBuilder} buildMatrix _ [] = do let defaultEnv = mkDefaultEnv- pure Environments {envDefault = fst defaultEnv, envProfiles = Map.fromList [defaultEnv], envStack = Nothing, envNix = Nothing}+ pure Environments {envDefault = fst defaultEnv, envProfiles = Map.fromList [defaultEnv], envStack = Nothing, envNix = Nothing, envBuilder = Nothing} mkDefaultEnv :: (Name, Enviroment) mkDefaultEnv =
src/HWM/Runtime/Files.hs view
@@ -10,7 +10,6 @@ statusM, aesonYAMLOptions, select,- remove, addHash, forbidOverride, cleanRelativePath,@@ -19,10 +18,12 @@ Signature, getFileSignature, prepareDir,+ syncFile,+ rewriteMaybe_, ) where -import Control.Exception (catch, throwIO, tryJust)+import Control.Exception (tryJust) import Control.Monad.Error.Class (MonadError (..)) import qualified Crypto.Hash.SHA256 as SHA256 import Data.Aeson@@ -41,14 +42,14 @@ import Data.Text (toTitle) import qualified Data.Text as T import qualified Data.Text.Encoding as T+import qualified Data.Text.IO as TIO import Data.Yaml (decodeThrow) import Data.Yaml.Pretty (defConfig, encodePretty, setConfCompare, setConfDropNull)-import HWM.Core.Formatting+import HWM.Core.Formatting (Format (..), Status (..)) import HWM.Core.Result (Issue) import Relude hiding (readFile, writeFile)-import System.Directory (createDirectoryIfMissing, doesFileExist, removeFile)+import System.Directory (createDirectoryIfMissing, doesFileExist) import System.FilePath (joinPath, splitDirectories)-import System.IO.Error (isDoesNotExistError) data Signature = Signed Text | Unsigned deriving (Ord, Show)@@ -73,9 +74,6 @@ safeIO :: IO a -> IO (Either String a) safeIO = tryJust (Just . printException) -remove :: (MonadIO m) => FilePath -> m ()-remove file = liftIO $ removeFile file `catch` (\e -> unless (isDoesNotExistError e) (throwIO e))- safeRead :: (MonadIO m) => FilePath -> m (Either String ByteString) safeRead = liftIO . safeIO . readFile @@ -114,12 +112,28 @@ withThrow :: (MonadError Issue m) => m (Either String a) -> m a withThrow x = x >>= either (throwError . fromString) pure -rewrite_ :: (MonadError Issue m, MonadIO m, FromJSON t, ToJSON t) => FilePath -> (Maybe t -> m t) -> m ()+rewrite_ :: (MonadError Issue m, MonadIO m, FromJSON t, ToJSON t) => FilePath -> (Maybe t -> m t) -> m Status rewrite_ pkg f = do- original <- safeRead pkg- yaml <- fromEither original >>= mapYaml f- withThrow (safeWrite pkg (serializeYaml yaml))+ prevYaml <- safeRead pkg >>= fromEither+ nextYaml <- mapYaml f prevYaml+ let same = Just (toJSON nextYaml) == (toJSON <$> prevYaml)+ if same+ then pure Checked+ else withThrow (safeWrite pkg (serializeYaml nextYaml)) $> Updated +rewriteMaybe_ :: (MonadError Issue m, MonadIO m, FromJSON t, ToJSON t) => FilePath -> (Maybe t -> m (Maybe t)) -> m Status+rewriteMaybe_ pkg f = do+ prevYaml <- safeRead pkg >>= fromEither+ maybeNext <- f (getData <$> prevYaml)+ case maybeNext of+ Nothing -> pure Checked -- nothing to change, so we consider it checked+ Just next -> do+ nextYaml <- mapYaml (const (pure next)) prevYaml+ let same = Just (toJSON nextYaml) == (toJSON <$> prevYaml)+ if same+ then pure Checked+ else withThrow (safeWrite pkg (serializeYaml nextYaml)) $> Updated+ statusM :: (MonadIO m) => FilePath -> m t -> m Status statusM pkg m = do before <- safeRead pkg@@ -240,3 +254,14 @@ prepareDir :: (MonadIO m) => FilePath -> m () prepareDir dir = liftIO $ createDirectoryIfMissing True dir++syncFile :: (MonadIO m) => FilePath -> Text -> m Status+syncFile path newFile = do+ exist <- liftIO $ doesFileExist path+ old <-+ if exist+ then Just <$> liftIO (TIO.readFile path)+ else pure Nothing+ if old == Just newFile+ then pure Checked+ else liftIO $ TIO.writeFile path newFile $> Updated
src/HWM/Runtime/Network.hs view
@@ -1,21 +1,31 @@ {-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE NoImplicitPrelude #-} -module HWM.Runtime.Network (uploadToGitHub, getGHUploadUrl) where+module HWM.Runtime.Network (uploadToGitHub, getGHUploadUrl, uploadToHackage, getHackageToken) where +import Control.Exception (try) import Control.Monad.Except (MonadError (..))+import Control.Retry (exponentialBackoff, limitRetries) import Data.Aeson (FromJSON) import qualified Data.Text as T-import HWM.Core.Result (Issue (..))+import HWM.Core.Pkg (Pkg (..))+import HWM.Core.Result (Issue (..), Severity (..)) import HWM.Domain.Config (Config (..))+import HWM.Domain.ConfigT (ConfigT)+import Network.HTTP.Client (HttpException (..), HttpExceptionContent (..), Response (responseStatus))+import Network.HTTP.Client.MultipartFormData (partFileSource) import Network.HTTP.Req+import Network.HTTP.Types (Status (..)) import Relude import System.FilePath (takeFileName)+import System.Timeout (timeout) import Text.URI (mkURI)+import UnliftIO.Retry (capDelay) getGitHubToken :: (MonadIO m, MonadError Issue m) => m Text getGitHubToken = do@@ -25,67 +35,106 @@ (pure . T.pack) maybeToken +ghAuth :: Text -> Option 'Https+ghAuth token =+ header "Authorization" ("Bearer " <> encodeUtf8 token)+ <> header "User-Agent" "hwm-tool"++hackageAuth :: Text -> Option 'Https+hackageAuth token =+ header "Authorization" ("X-ApiKey " <> encodeUtf8 token)+ <> header "User-Agent" "hwm-tool"+ uploadToGitHub :: (MonadIO m, MonadError Issue m) => Text -> FilePath -> m () uploadToGitHub uploadUrl filePath = do token <- getGitHubToken liftIO $ runReq defaultHttpConfig $ do- uri <- liftIO $ mkURI uploadUrl- case useHttpsURI uri of- Just (url, opts) -> do- let fileName = T.pack $ takeFileName filePath- void- $ req- POST- url- (ReqBodyFile filePath) -- Raw bytes, no multipart wrapping- ignoreResponse- ( opts- <> queryParam "name" (Just fileName) -- Required by GitHub- <> header "Authorization" ("Bearer " <> encodeUtf8 token)- <> header "Content-Type" "application/octet-stream" -- Crucial!- <> header "User-Agent" "hwm-tool" -- GitHub requires a User-Agent- )- Nothing -> liftIO $ putStrLn "GitHub Upload URLs must be HTTPS"---- 1. Define a tiny data type to represent the GitHub JSON response.--- Aeson automatically maps the "upload_url" JSON key to this record field.+ (url, opts) <- withURI uploadUrl+ void+ $ req+ POST+ url+ (ReqBodyFile filePath)+ ignoreResponse+ ( opts+ <> queryParam "name" (Just $ T.pack $ takeFileName filePath)+ <> ghAuth token+ <> header "Content-Type" "application/octet-stream"+ ) data GitHubRelease = GitHubRelease { name :: Text, upload_url :: Text }- deriving (Show, Generic)---- 2. Automatically derive the JSON parser-instance FromJSON GitHubRelease+ deriving (Show, Generic, FromJSON) --- 3. The main function returning the clean URL getGHUploadUrl :: (MonadIO m, MonadError Issue m) => Config -> Text -> m Text getGHUploadUrl Config {..} tag = do gh <- maybe (throwError "GitHub repository not configured") pure cfgGithub token <- getGitHubToken liftIO $ runReq defaultHttpConfig $ do- -- Construct the endpoint URL- let urlStr = "https://api.github.com/repos/" <> gh <> "/releases/tags/" <> tag- uri <- liftIO $ mkURI urlStr- case useHttpsURI uri of- Just (url, opts) -> do- -- Execute the GET request, expecting a JSON response matching our GitHubRelease type- r <-- req- GET- url- NoReqBody- jsonResponse -- This automatically parses the ByteString into our GitHubRelease data type!- ( opts- <> header "Authorization" ("Bearer " <> encodeUtf8 token)- <> header "Accept" "application/vnd.github+json"- <> header "User-Agent" "hwm-tool"- )+ (url, opts) <- withURI ("https://api.github.com/repos/" <> gh <> "/releases/tags/" <> tag)+ r <- req GET url NoReqBody jsonResponse (opts <> ghAuth token <> header "Accept" "application/vnd.github+json")+ let rawUrl = upload_url (responseBody r)+ pure $ T.takeWhile (/= '{') rawUrl - -- Extract the raw URL from the parsed JSON object- let rawUrl = upload_url (responseBody r)+withURI :: (MonadIO m) => Text -> m (Url Https, Option scheme)+withURI u = do+ uri <- liftIO $ mkURI u+ case useHttpsURI uri of+ Just (url, opts) -> pure (url, opts)+ Nothing -> error ("URLs must be HTTPS: " <> show u) - -- Strip the "{?name,label}" template suffix before returning- return $ T.takeWhile (/= '{') rawUrl- Nothing -> error "GitHub API URLs must be HTTPS"+mkSecond :: Int -> Int+mkSecond n = n * 1000000++hwmConfig :: HttpConfig+hwmConfig =+ let policy =+ capDelay (mkSecond 5) (exponentialBackoff (mkSecond 1))+ <> limitRetries 3+ in defaultHttpConfig {httpConfigRetryPolicy = policy}++safeReq :: (MonadIO m) => Pkg -> Text -> (Status -> Issue) -> IO b -> m (Either Issue b)+safeReq pkg errMsg f action = do+ res <- liftIO $ timeout (mkSecond 90) $ try action+ case res of+ Nothing -> pure $ Left $ Issue (pkgMemberId pkg) SeverityError (errMsg <> " (Timeout)") Nothing+ Just (Left (VanillaHttpException (HttpExceptionRequest _ (StatusCodeException r _)))) -> pure $ Left $ f (responseStatus r)+ Just (Left e) -> pure $ Left $ networkError pkg e+ Just (Right a) -> pure $ Right a++getHackageToken :: (MonadIO m, MonadError Issue m) => m Text+getHackageToken = do+ maybeToken <- liftIO $ lookupEnv "HACKAGE_AUTH_TOKEN"+ maybe+ (throwError "HACKAGE_AUTH_TOKEN environment variable not set. Please set it to a valid Hackage API Token.")+ (pure . T.pack)+ maybeToken++uploadToHackage :: Text -> Pkg -> FilePath -> ConfigT [Issue]+uploadToHackage token pkg tarballPath = do+ let auth = hackageAuth token+ let url = https "hackage.haskell.org" /: "packages"+ body <- reqBodyMultipart [partFileSource "package" tarballPath]+ result <-+ safeReq pkg "Hackage Upload Failed" (handleHttpError pkg)+ $ runReq hwmConfig+ $ req POST url body ignoreResponse auth+ pure $ either pure (const []) result++handleHttpError :: Pkg -> Status -> Issue+handleHttpError pkg status = case statusCode status of+ 409 -> warn pkg "Version already exists on Hackage. Skipping."+ 401 -> err pkg "Invalid Hackage API Token."+ 413 -> err pkg "Package tarball is too large for Hackage."+ _ -> err pkg $ "Hackage returned: " <> show (statusCode status)++networkError :: (Show a) => Pkg -> a -> Issue+networkError pkg _ = Issue (pkgMemberId pkg) SeverityError "Hackage Connection Error" Nothing++err :: Pkg -> Text -> Issue+err pkg msg = Issue (pkgMemberId pkg) SeverityError msg Nothing++warn :: Pkg -> Text -> Issue+warn pkg msg = Issue (pkgMemberId pkg) SeverityWarning msg Nothing
src/HWM/Runtime/UI.hs view
@@ -26,6 +26,7 @@ runSpinner, printGenTable, forTable,+ statusTableM, ) where @@ -36,7 +37,7 @@ import qualified Data.Map.Strict as Map import qualified Data.Text as T import HWM.Core.Common (Name)-import HWM.Core.Formatting (Color (..), Format (..), Status (..), chalk, indentBlockNum, padDots, renderSummaryStatus, subPathSign)+import HWM.Core.Formatting (Color (..), Format (..), Status (..), chalk, indentBlockNum, padDots, renderSummaryStatus, statusIcon, subPathSign) import HWM.Core.Result ( Issue (..), IssueDetails (..),@@ -134,11 +135,18 @@ tableM_ :: (MonadUI m) => [(Text, m Text)] -> m () tableM_ rows = forHLTable rows (second ((,()) <$>)) $> () +statusTableM :: (MonadUI m) => [(Text, m Status)] -> m ()+statusTableM = tableM_ . map (second render)+ where+ render s = statusIcon <$> s+ sectionTableM :: (MonadUI m) => Text -> [(Text, m Text)] -> m () sectionTableM title = section title . tableM_ -sectionConfig :: (MonadUI m) => [(Text, m Text)] -> m ()-sectionConfig = section "config" . tableM_+sectionConfig :: (MonadUI m) => [(Text, m Status)] -> m ()+sectionConfig = section "config" . tableM_ . map (second render)+ where+ render s = statusIcon <$> s printGenTable :: (MonadUI m) => [[Text]] -> m () printGenTable rows =
− test/Commands/Init.hs
@@ -1,21 +0,0 @@-module Commands.Init (testInit) where--import Test.Hspec (Spec, describe, it)-import Utils.Golden (Golden (..), goldenTest)--testInit :: Spec-testInit = describe "init" $ do- it "inits a simple workspace" $- goldenTest- Golden- { cmd = "init",- project = "simple",- scenario = "init/simple"- }- it "inits a huge monorepo" $- goldenTest- Golden- { cmd = "init",- project = "morpheus",- scenario = "init/cabal"- }
− test/Commands/Sync.hs
@@ -1,35 +0,0 @@-module Commands.Sync (testSync) where--import Test.Hspec (Spec, describe, it)-import Utils.Golden (Golden (..), goldenTest)--testSync :: Spec-testSync = describe "sync" $ do- it "syncs a simple workspace" $- goldenTest- Golden- { cmd = "sync",- project = "simple",- scenario = "sync/simple"- }- it "syncs a cabal-only workspace" $- goldenTest- Golden- { cmd = "sync",- project = "morpheus",- scenario = "sync/cabal"- }- it "syncs a nix workspace" $- goldenTest- Golden- { cmd = "sync",- project = "morpheus",- scenario = "sync/nix"- }- it "syncs a stack workspace" $- goldenTest- Golden- { cmd = "sync",- project = "morpheus",- scenario = "sync/stack"- }
− test/Main.hs
@@ -1,10 +0,0 @@-module Main (main) where--import Commands.Init (testInit)-import Commands.Sync (testSync)-import Test.Hspec (hspec)--main :: IO ()-main = hspec $ do- testInit- testSync
− test/Utils/Core.hs
@@ -1,88 +0,0 @@-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE NoImplicitPrelude #-}--module Utils.Core (assertNotModified, assertWorkspaceNotModified, copyLocalFiles, inWorkDir, diff, runHWM) where--import Control.Concurrent (threadDelay)-import qualified Data.List as S-import qualified GHC.IO.Exception as System.Exit-import Relude-import System.Directory (createDirectoryIfMissing, doesDirectoryExist, getCurrentDirectory, getModificationTime, listDirectory, makeAbsolute, setCurrentDirectory)-import System.Directory.Internal.Prelude (bracket)-import System.FilePath (takeExtension, (</>))-import System.IO.Temp (withSystemTempDirectory)-import System.Process (callCommand, readCreateProcessWithExitCode, shell)-import Test.Hspec (Expectation, expectationFailure, shouldBe)--assertNotModified :: FilePath -> IO () -> Expectation-assertNotModified path action = do- oldTime <- getModificationTime path- threadDelay 1100000- action- newTime <- getModificationTime path- newTime `shouldBe` oldTime--assertWorkspaceNotModified :: FilePath -> IO a -> Expectation-assertWorkspaceNotModified root action = do- managedFiles <- findManagedFiles root- oldTimes <- mapM (\p -> (p,) <$> getModificationTime p) managedFiles- threadDelay 1100000- _ <- action- newTimes <- mapM (\p -> (p,) <$> getModificationTime p) managedFiles- newTimes `shouldBe` oldTimes---- | Helper to find files HWM cares about (.cabal, .yaml, .nix, .project)-findManagedFiles :: FilePath -> IO [FilePath]-findManagedFiles dir = do- contents <- listDirectory dir- paths <- mapM (\path -> let p = dir </> path in (p,) <$> doesDirectoryExist p) contents- let files = [p | (p, isDir) <- paths, not isDir, isManagedExtension p]- subDirFiles <- concat <$> mapM (\(p, isDir) -> if isDir then findManagedFiles p else return []) paths- return $ files ++ subDirFiles- where- isManagedExtension p = takeExtension p `elem` [".cabal", ".yaml", ".nix", ".project"]--copyLocalFiles :: FilePath -> IO ()-copyLocalFiles = copyDir "."--copyDir :: FilePath -> FilePath -> IO ()-copyDir src dst = do- createDirectoryIfMissing True dst- callCommand $ "cp -r " <> src <> " " <> dst--copyFrom :: FilePath -> FilePath -> IO ()-copyFrom src = copyDir (src <> "/.")--inWorkDir :: FilePath -> FilePath -> IO a -> IO ()-inWorkDir project scenario m = do- projectDir <- makeAbsolute ("test/projects/" </> project)- overridesDir <- makeAbsolute (scenario </> "overrides")- withSystemTempDirectory "hwm-golden" $ \tmpDir -> do- let workDir = tmpDir </> "work"- copyFrom projectDir workDir- whenM (doesDirectoryExist overridesDir) $ copyFrom overridesDir workDir- bracket getCurrentDirectory setCurrentDirectory $ \_ -> do- setCurrentDirectory workDir- m $> ()--diff :: FilePath -> [FilePath] -> IO ()-diff expectedDir ignoreList = do- let ignoreFlags = S.unwords ["-x " ++ p | p <- ignoreList]- (diffCode, diffOut, _) <-- readCreateProcessWithExitCode- (shell $ "diff -ruN " ++ ignoreFlags ++ " " ++ expectedDir ++ " .")- ""- unless (diffCode == System.Exit.ExitSuccess)- $ expectationFailure- $ "File diff failed:\n"- <> diffOut--runHWM :: String -> IO String-runHWM cmd = do- (exitCode, out, err) <-- readCreateProcessWithExitCode- (shell $ "stack exec hwm -- " <> cmd)- ""- unless (exitCode == System.Exit.ExitSuccess)- $ expectationFailure ("Command failed with stdout: " <> out <> "stderr: " <> err)- return out
− test/Utils/Golden.hs
@@ -1,38 +0,0 @@-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE NoImplicitPrelude #-}--module Utils.Golden (Golden (..), goldenTest) where--import Relude-import System.Directory (makeAbsolute, removePathForcibly)-import System.FilePath ((</>))-import qualified System.IO as IO-import Test.Hspec (Expectation, shouldBe)-import Utils.Core (copyLocalFiles, diff, inWorkDir, runHWM)--data Golden = Golden- { cmd :: String,- scenario :: FilePath,- project :: FilePath- }--isUpdateMode :: IO Bool-isUpdateMode = (== Just "1") <$> lookupEnv "GOLDEN_UPDATE"--goldenTest :: Golden -> Expectation-goldenTest Golden {..} = do- scenarioDir <- makeAbsolute $ "test/golden/" </> scenario- let expectedDir = scenarioDir </> "expected"- let stdoutFile = scenarioDir </> "stdout.ansi"- updateMode <- isUpdateMode- inWorkDir project scenarioDir $ do- out <- runHWM cmd- if updateMode- then do- removePathForcibly expectedDir- copyLocalFiles expectedDir- IO.writeFile stdoutFile out- else do- diff expectedDir [".hwm", ".stack-work", "dist-newstyle", "*.log"]- expectedStdout <- IO.readFile stdoutFile- out `shouldBe` expectedStdout