stacker 0.0.0.0 → 0.0.1.0
raw patch · 20 files changed
+425/−76 lines, 20 filesdep +tasty-hunitPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: tasty-hunit
API changes (from Hackage documentation)
- Types: [actionNewSnapshot] :: Action -> Maybe Text
+ Analysis: determineNewSnapshot :: SnapshotDB -> Text -> Bump
+ Types: BumpTo :: Text -> Bump
+ Types: InfoAll :: InfoCmd
+ Types: InfoRepo :: InfoCmd
+ Types: InfoSnapshots :: InfoCmd
+ Types: Unknown :: Bump
+ Types: UpToDate :: Bump
+ Types: [actionBump] :: Action -> Bump
+ Types: data Bump
+ Types: data InfoCmd
+ Types: instance GHC.Classes.Eq Types.Bump
+ Types: instance GHC.Classes.Eq Types.InfoCmd
+ Types: instance GHC.Generics.Generic Types.Bump
+ Types: instance GHC.Show.Show Types.Bump
+ Types: instance GHC.Show.Show Types.InfoCmd
- Types: Action :: FilePath -> Text -> Maybe Text -> Bool -> (Int, Int) -> Maybe FilePath -> Action
+ Types: Action :: FilePath -> Text -> Bump -> Bool -> (Int, Int) -> Maybe FilePath -> Action
- Types: Info :: Command
+ Types: Info :: InfoCmd -> Command
Files
- CHANGELOG.md +25/−0
- README.md +10/−2
- data/ghc.csv +2/−2
- data/lts.csv +27/−0
- data/nightly.csv +157/−0
- src/Analysis.hs +28/−20
- src/CSV.hs +42/−4
- src/Commands.hs +27/−21
- src/Options.hs +13/−6
- src/PathUtil.hs +2/−2
- src/StackYaml.hs +9/−8
- src/Types.hs +16/−2
- stacker.cabal +6/−1
- test/Main.hs +3/−1
- test/TestAnalysis.hs +51/−0
- test/golden/dry-run-recursive.golden +2/−2
- test/golden/dry-run.golden +2/−2
- test/golden/tests/stack-9.10.yaml +1/−1
- test/golden/tests/stack-9.12.yaml +1/−1
- test/golden/tests/stack.yaml +1/−1
CHANGELOG.md view
@@ -1,3 +1,28 @@+0.0.1.0+=======++* Fix: a nightly snapshot missing from the database (e.g. a date for which+ no nightly was published) was reported as `✓ up to date`.+ Now its GHC version is inferred from the closest known nightly.++* Snapshots that cannot be analyzed are now reported as `? unknown snapshot`+ instead of `✓ up to date`. This includes snapshots newer than the database+ (previously a downgrade could be suggested), unknown LTS major versions,+ and resolvers that are not Stackage snapshots.++* `stacker update` is much faster (seconds instead of minutes): the GHC version+ is now read from the end of each snapshot file instead of parsing the whole YAML.+ If this fails for a file, a warning is printed and full YAML parsing is used.++* New subcommands `stacker info repo` and `stacker info snapshots`+ that print just the repository path and just the snapshot list, respectively.++* Shipped snapshot database updated (latest: lts-24.63, nightly-2026-10-12).++* Tested with GHC 9.4.8 - 9.14.1.++_Andreas Abel, 2026-10-15_+ 0.0.0.0 =======
README.md view
@@ -61,7 +61,7 @@ | `dry-run [FILES...]` | Shows what updates would be performed by the respective `bump` (default command) | | `config` | Configures `stacker` | | `update` | Updates its database of Stackage snapshots |-| `info` | Prints table mapping GHC major versions to their latest snapshots |+| `info [repo\|snapshots]` | Prints repository path and/or table mapping GHC major versions to their latest snapshots | These can be modified with the following option(s): @@ -189,7 +189,7 @@ stack-9.10.yaml nightly-2024-07-07 → bump to lts-24.36 stack-9.12.yaml nightly-2025-10-03 → bump to nightly-2026-04-01 ```-When coloring is on, the `stack*.yml` column is printed in bold, `✓ up to date` in green and any bump action in bright yellow.+When coloring is on, the `stack*.yml` column is printed in bold, `✓ up to date` in green, any bump action in bright yellow, and `? unknown snapshot` in red. Internally, `bump` and `dry-run` share the analysis phase that reads the `stack*.yaml` and creates for each an action plan, yet `dry-run` just pretty-prints this plan as detailed above whereas `bump` executes it.@@ -254,6 +254,14 @@ 9.10.3: lts-24.36 9.12.4: nightly-2026-04-01 ```++These can also be printed separately:+- `stacker info repo` prints just the bare path `$REPO`.+- `stacker info snapshots` prints just the map, without indentation, e.g.+ ```+ 9.8.4: lts-23.28+ 9.10.3: lts-24.36+ ``` ## Testing
data/ghc.csv view
@@ -53,7 +53,7 @@ 9.8.4,lts-23.28 9.10.1,nightly-2025-05-05 9.10.2,lts-24.11-9.10.3,lts-24.36+9.10.3,lts-24.63 9.12.2,nightly-2025-12-30 9.12.3,nightly-2026-03-28-9.12.4,nightly-2026-04-07+9.12.4,nightly-2026-10-12
data/lts.csv view
@@ -608,3 +608,30 @@ 24.34,9.10.3 24.35,9.10.3 24.36,9.10.3+24.37,9.10.3+24.38,9.10.3+24.39,9.10.3+24.40,9.10.3+24.41,9.10.3+24.42,9.10.3+24.43,9.10.3+24.44,9.10.3+24.45,9.10.3+24.46,9.10.3+24.47,9.10.3+24.48,9.10.3+24.49,9.10.3+24.50,9.10.3+24.51,9.10.3+24.52,9.10.3+24.53,9.10.3+24.54,9.10.3+24.55,9.10.3+24.56,9.10.3+24.57,9.10.3+24.58,9.10.3+24.59,9.10.3+24.60,9.10.3+24.61,9.10.3+24.62,9.10.3+24.63,9.10.3
data/nightly.csv view
@@ -3094,3 +3094,160 @@ 2026-04-05,9.12.4 2026-04-06,9.12.4 2026-04-07,9.12.4+2026-04-09,9.12.4+2026-04-10,9.12.4+2026-04-11,9.12.4+2026-04-12,9.12.4+2026-04-14,9.12.4+2026-04-15,9.12.4+2026-04-16,9.12.4+2026-04-17,9.12.4+2026-04-18,9.12.4+2026-04-19,9.12.4+2026-04-20,9.12.4+2026-04-21,9.12.4+2026-04-22,9.12.4+2026-04-23,9.12.4+2026-04-24,9.12.4+2026-04-25,9.12.4+2026-04-26,9.12.4+2026-04-28,9.12.4+2026-04-29,9.12.4+2026-04-30,9.12.4+2026-05-01,9.12.4+2026-05-02,9.12.4+2026-05-03,9.12.4+2026-05-04,9.12.4+2026-05-05,9.12.4+2026-05-06,9.12.4+2026-05-10,9.12.4+2026-05-11,9.12.4+2026-05-12,9.12.4+2026-05-14,9.12.4+2026-05-15,9.12.4+2026-05-16,9.12.4+2026-05-17,9.12.4+2026-05-18,9.12.4+2026-05-19,9.12.4+2026-05-21,9.12.4+2026-05-22,9.12.4+2026-05-23,9.12.4+2026-05-24,9.12.4+2026-05-25,9.12.4+2026-05-26,9.12.4+2026-05-27,9.12.4+2026-05-29,9.12.4+2026-06-01,9.12.4+2026-06-02,9.12.4+2026-06-03,9.12.4+2026-06-04,9.12.4+2026-06-05,9.12.4+2026-06-06,9.12.4+2026-06-07,9.12.4+2026-06-08,9.12.4+2026-06-09,9.12.4+2026-06-10,9.12.4+2026-06-11,9.12.4+2026-06-12,9.12.4+2026-06-13,9.12.4+2026-06-14,9.12.4+2026-06-15,9.12.4+2026-06-16,9.12.4+2026-06-18,9.12.4+2026-06-19,9.12.4+2026-06-20,9.12.4+2026-06-21,9.12.4+2026-06-22,9.12.4+2026-06-25,9.12.4+2026-06-26,9.12.4+2026-06-27,9.12.4+2026-06-28,9.12.4+2026-06-29,9.12.4+2026-06-30,9.12.4+2026-07-01,9.12.4+2026-07-02,9.12.4+2026-07-03,9.12.4+2026-07-06,9.12.4+2026-07-07,9.12.4+2026-07-08,9.12.4+2026-07-09,9.12.4+2026-07-10,9.12.4+2026-07-11,9.12.4+2026-07-12,9.12.4+2026-07-13,9.12.4+2026-07-15,9.12.4+2026-07-16,9.12.4+2026-07-17,9.12.4+2026-07-18,9.12.4+2026-07-19,9.12.4+2026-07-20,9.12.4+2026-07-21,9.12.4+2026-07-22,9.12.4+2026-07-23,9.12.4+2026-07-24,9.12.4+2026-07-26,9.12.4+2026-07-27,9.12.4+2026-07-28,9.12.4+2026-07-29,9.12.4+2026-07-30,9.12.4+2026-07-31,9.12.4+2026-08-01,9.12.4+2026-08-02,9.12.4+2026-08-03,9.12.4+2026-08-04,9.12.4+2026-08-05,9.12.4+2026-08-06,9.12.4+2026-08-07,9.12.4+2026-08-08,9.12.4+2026-08-09,9.12.4+2026-08-10,9.12.4+2026-08-11,9.12.4+2026-08-12,9.12.4+2026-08-13,9.12.4+2026-08-14,9.12.4+2026-08-15,9.12.4+2026-08-16,9.12.4+2026-08-19,9.12.4+2026-08-20,9.12.4+2026-08-23,9.12.4+2026-08-24,9.12.4+2026-08-25,9.12.4+2026-08-26,9.12.4+2026-08-27,9.12.4+2026-08-30,9.12.4+2026-08-31,9.12.4+2026-09-01,9.12.4+2026-09-06,9.12.4+2026-09-07,9.12.4+2026-09-08,9.12.4+2026-09-09,9.12.4+2026-09-10,9.12.4+2026-09-11,9.12.4+2026-09-12,9.12.4+2026-09-13,9.12.4+2026-09-14,9.12.4+2026-09-15,9.12.4+2026-09-16,9.12.4+2026-09-18,9.12.4+2026-09-19,9.12.4+2026-09-20,9.12.4+2026-09-21,9.12.4+2026-09-22,9.12.4+2026-09-23,9.12.4+2026-09-24,9.12.4+2026-09-25,9.12.4+2026-09-26,9.12.4+2026-09-27,9.12.4+2026-09-29,9.12.4+2026-09-30,9.12.4+2026-10-01,9.12.4+2026-10-02,9.12.4+2026-10-03,9.12.4+2026-10-04,9.12.4+2026-10-05,9.12.4+2026-10-06,9.12.4+2026-10-08,9.12.4+2026-10-09,9.12.4+2026-10-10,9.12.4+2026-10-11,9.12.4+2026-10-12,9.12.4
src/Analysis.hs view
@@ -4,22 +4,24 @@ ( analyzeStackYaml , analyzeAllStackYamls , analyzeStackYamls+ , determineNewSnapshot ) where import Prelude hiding (min, span) +import Control.Applicative ((<|>)) import Control.Monad (filterM) import Data.Functor ((<&>)) import Data.List (maximumBy) import Data.Map.Strict qualified as Map-import Data.Maybe (catMaybes)+import Data.Maybe (catMaybes, fromMaybe) import Data.Ord (comparing) import Data.Text (Text) import Data.Text qualified as T import System.Directory (doesFileExist) import Text.Printf (printf) -import Types (Action(..), SnapshotDB(..), LTSVersion(..), NightlyVersion(..), GHCVersion(..), Snapshot(..))+import Types (Action(..), Bump(..), SnapshotDB(..), LTSVersion(..), NightlyVersion(..), GHCVersion(..), Snapshot(..)) import StackYaml (parseStackYaml, findStackYamlFiles, findStackYamlFilesRecursive, getSymlinkMap) -- | Analyze a single stack.yaml file@@ -40,8 +42,8 @@ analyzeStackYamls db files recursive = do -- Get files to analyze: either auto-discover or use provided list -- Non-existent files are silently filtered out- filesToAnalyze <- - if null files + filesToAnalyze <-+ if null files then if recursive then findStackYamlFilesRecursive else findStackYamlFiles@@ -55,14 +57,23 @@ analyzeAllStackYamls db = analyzeStackYamls db [] False -- | Determine the new snapshot for a given old snapshot-determineNewSnapshot :: SnapshotDB -> Text -> Maybe Text+determineNewSnapshot :: SnapshotDB -> Text -> Bump determineNewSnapshot db oldSnap- | T.isPrefixOf "lts-" oldSnap = determineLTSBump db oldSnap- | T.isPrefixOf "nightly-" oldSnap = determineNightlyBump db oldSnap- | otherwise = Nothing+ | T.isPrefixOf "lts-" oldSnap = fromMaybe Unknown $ determineLTSBump db oldSnap+ | T.isPrefixOf "nightly-" oldSnap = fromMaybe Unknown $ determineNightlyBump db oldSnap+ | otherwise = Unknown +-- | Compare the latest known snapshot with the old one.+-- If the old snapshot is newer than anything we know of,+-- our database is outdated and we cannot give advice.+compareWithLatest :: Ord a => a -> a -> Snapshot -> Bump+compareWithLatest old latest snap = case compare latest old of+ GT -> BumpTo (formatSnapshot snap)+ EQ -> UpToDate+ LT -> Unknown+ -- | Determine bump for LTS snapshot-determineLTSBump :: SnapshotDB -> Text -> Maybe Text+determineLTSBump :: SnapshotDB -> Text -> Maybe Bump determineLTSBump db oldSnap = do oldVersion <- parseLTSSnapshot oldSnap -- Find the latest LTS with the same major version@@ -71,16 +82,16 @@ then Nothing else do let (latestVersion, _) = maximumBy (comparing fst) sameMajor- let newSnap = formatSnapshot (LTS latestVersion)- if newSnap == oldSnap- then Nothing -- Already up to date- else Just newSnap+ return $ compareWithLatest oldVersion latestVersion (LTS latestVersion) -- | Determine bump for nightly snapshot-determineNightlyBump :: SnapshotDB -> Text -> Maybe Text+determineNightlyBump :: SnapshotDB -> Text -> Maybe Bump determineNightlyBump db oldSnap = do oldVersion <- parseNightlySnapshot oldSnap- oldGHC <- Map.lookup oldVersion (dbNightly db)+ -- The exact nightly may be missing from the database+ -- (e.g. a date for which no nightly was published);+ -- then take the GHC version of the closest known neighbour.+ (_, oldGHC) <- Map.lookupLE oldVersion (dbNightly db) <|> Map.lookupGE oldVersion (dbNightly db) -- Get GHC major version (e.g., (9,6) from GHCVersion 9 6 1) let ghcMajor = getGHCMajor oldGHC -- Find if there's an LTS for this GHC major version@@ -89,7 +100,7 @@ then do -- Bump to latest LTS for this GHC major let (latestLTS, _) = maximumBy (comparing fst) ltsForGHC- return $ formatSnapshot (LTS latestLTS)+ return $ BumpTo $ formatSnapshot (LTS latestLTS) else do -- Bump to latest nightly for this GHC major let nightliesForGHC = filter (\(_, ghc) -> getGHCMajor ghc == ghcMajor) $ Map.toList (dbNightly db)@@ -97,10 +108,7 @@ then Nothing else do let (latestNightly, _) = maximumBy (comparing fst) nightliesForGHC- let newSnap = formatSnapshot (Nightly latestNightly)- if newSnap == oldSnap- then Nothing- else Just newSnap+ return $ compareWithLatest oldVersion latestNightly (Nightly latestNightly) -- | Get GHC major version from full version (e.g., (9,6) from GHCVersion 9 6 1) getGHCMajor :: GHCVersion -> (Int, Int)
src/CSV.hs view
@@ -9,7 +9,8 @@ import Prelude hiding (lines, min) import Control.Monad (unless)-import Data.List (sortBy, sortOn)+import Data.ByteString.Char8 qualified as BS+import Data.List (find, sortBy, sortOn) import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Ord (comparing)@@ -25,7 +26,7 @@ import Text.Printf (printf) import Types (LTSVersion(..), NightlyVersion(..), GHCVersion(..), Snapshot(..), SnapshotDB(..)) import XDG (getStateDir)-import System.IO (stderr)+import System.IO (IOMode(ReadMode), SeekMode(AbsoluteSeek), hFileSize, hSeek, stderr, withBinaryFile) import Paths_stacker (getDataDir) import System.Directory qualified as Dir @@ -83,7 +84,7 @@ stateDir <- getStateDir createDirectoryIfMissing True stateDir - TIO.hPutStrLn stderr $ "Processing snapshot database. This might take a couple of minutes..."+ TIO.hPutStrLn stderr $ "Processing snapshot database..." -- Process LTS snapshots TIO.hPutStrLn stderr $ "Processing LTSs..."@@ -165,9 +166,46 @@ | otherwise = error $ "Unexpected file in nightly directory: " ++ fname ++ " (expected .yaml extension)" --- | Extract GHC version from a snapshot YAML file+-- | Extract GHC version from a snapshot YAML file.+-- Tries the fast method first, falling back to (slow) full YAML parsing with a warning. extractGHCVersion :: FilePath -> IO GHCVersion extractGHCVersion file = do+ fast <- extractGHCVersionFast file+ case fast of+ Just ghc -> return ghc+ Nothing -> do+ TIO.hPutStrLn stderr $ "Warning: could not find compiler field at the end of " <> T.pack file+ <> ", falling back to slow YAML parsing"+ extractGHCVersionYaml file++-- | Number of bytes at the end of a snapshot file to search for the compiler field+tailSize :: Integer+tailSize = 4096++-- | Extract GHC version by searching the end of a snapshot YAML file for the compiler field.+-- This avoids parsing the whole (large) file. The compiler field is the last entry,+-- either at top level (old format) or under @resolver:@ (new format):+--+-- > compiler: ghc-7.8.3+--+-- > resolver:+-- > compiler: ghc-9.12.4+extractGHCVersionFast :: FilePath -> IO (Maybe GHCVersion)+extractGHCVersionFast file = do+ chunk <- withBinaryFile file ReadMode $ \h -> do+ size <- hFileSize h+ hSeek h AbsoluteSeek (max 0 (size - tailSize))+ BS.hGetContents h+ return $ do+ line <- find isCompilerLine $ reverse $ BS.lines chunk+ parseGHCVersionText $ T.pack $ BS.unpack $ BS.strip $ BS.drop (BS.length prefix) $ BS.dropWhile (== ' ') line+ where+ prefix = "compiler: ghc-"+ isCompilerLine l = any (`BS.isPrefixOf` l) [prefix, " " <> prefix]++-- | Extract GHC version from a snapshot YAML file by full YAML parsing+extractGHCVersionYaml :: FilePath -> IO GHCVersion+extractGHCVersionYaml file = do result <- Yaml.decodeFileEither file case result of Left err -> error $ "Failed to parse " ++ file ++ ": " ++ show err
src/Commands.hs view
@@ -48,7 +48,7 @@ putStrLn " dry-run [FILES...] Show what would be updated by 'bump' (default)" putStrLn " Use --recursive/-r to search subdirectories" putStrLn " update Update stackage snapshots database"- putStrLn " info Print GHC version to snapshot mapping"+ putStrLn " info [repo|snapshots] Print repository path and/or GHC version to snapshot mapping" putStrLn " config Configure stacker" putStrLn "" putStrLn "Available options:"@@ -70,6 +70,7 @@ putStrLn "" putStrLn "For more information, see the README" Config configCmd -> runConfig configCmd -- Handle config first!+ Info InfoRepo -> getRepoPath >>= putStrLn -- Does not need CSV files cmd -> runEssentialCommand useColor cmd -- | Run an essential command (requires CSV files)@@ -86,10 +87,7 @@ repoPath <- getRepoPath ensureRepo repoPath runUpdate repoPath- Info -> do- -- Info command shows the repo path- repoPath <- getRepoPath- runInfo repoPath+ Info infoCmd -> runInfo infoCmd _ -> return () -- | Print version information@@ -111,13 +109,13 @@ -- Validate: recursive cannot be used with explicit files when (recursive && not (null files)) $ die "Error: --recursive cannot be used with explicit file arguments"- + db <- loadSnapshotDB actions <- analyzeStackYamls db files recursive -- Sort actions by filename let sortedActions = sortBy (comparing actionFile) actions- + -- Calculate the maximum filename length for proper alignment (add 2 for padding) let maxFileLen = case sortedActions of [] -> 20@@ -131,7 +129,7 @@ printActionWithWidth useColor fileWidth action = do let file = actionFile action let oldSnap = actionOldSnapshot action- let newSnap = actionNewSnapshot action+ let bump = actionBump action let symlinkTarget = actionSymlinkTarget action -- Print with proper alignment (file padded to fileWidth chars, oldSnap to 25 chars)@@ -147,11 +145,14 @@ putStr "= symlink to " putStr target Nothing ->- case newSnap of- Nothing -> do+ case bump of+ UpToDate -> do withColor useColor [SetColor Foreground Vivid Green] $ putStr "✓ up to date"- Just new -> do+ Unknown -> do+ withColor useColor [SetColor Foreground Vivid Red] $+ putStr "? unknown snapshot"+ BumpTo new -> do withColor useColor [SetColor Foreground Vivid Yellow] $ do putStr "→ bump to " putStr $ T.unpack new@@ -168,7 +169,7 @@ -- Validate: recursive cannot be used with explicit files when (recursive && not (null files)) $ die "Error: --recursive cannot be used with explicit file arguments"- + db <- loadSnapshotDB actions <- analyzeStackYamls db files recursive mapM_ (applyAction True) actions@@ -181,15 +182,20 @@ generateCSVs repoPath -- | Run info command-runInfo :: FilePath -> IO ()-runInfo repoPath = do- putStrLn $ "repo: " ++ repoPath- db <- loadSnapshotDB- putStrLn "snapshots:"-- let ghcEntries = Map.toAscList (dbGHC db)- forM_ ghcEntries $ \(ghc, snapshot) -> do- putStrLn $ " " ++ formatGHCVersionText ghc ++ ": " ++ T.unpack (formatSnapshotText snapshot)+runInfo :: InfoCmd -> IO ()+runInfo = \case+ InfoAll -> do+ repoPath <- getRepoPath+ putStrLn $ "repo: " ++ repoPath+ putStrLn "snapshots:"+ printSnapshots " "+ InfoRepo -> getRepoPath >>= putStrLn+ InfoSnapshots -> printSnapshots ""+ where+ printSnapshots indent = do+ db <- loadSnapshotDB+ forM_ (Map.toAscList (dbGHC db)) $ \(ghc, snapshot) ->+ putStrLn $ indent ++ formatGHCVersionText ghc ++ ": " ++ T.unpack (formatSnapshotText snapshot) -- | Format GHC version as text formatGHCVersionText :: GHCVersion -> String
src/Options.hs view
@@ -36,27 +36,27 @@ -- Helper to combine a command parser with the color option, creating an Options value withColorOption :: Parser Command -> Parser Options withColorOption cmdParser = Options <$> cmdParser <*> colorOption- + -- Subcommand parser that includes color options for each command subcommandWithOptions = subparser ( command "bump" (info (withColorOption bumpParser) (progDesc "Update stack*.yaml files (optionally specify files, or use --recursive/-r to search subdirectories)")) <> command "dry-run" (info (withColorOption dryRunParser) (progDesc "Show what would be updated (default, optionally specify files, or use --recursive/-r to search subdirectories)")) <> command "update" (info (withColorOption (pure Update)) (progDesc "Update stackage snapshots database"))- <> command "info" (info (withColorOption (pure Info)) (progDesc "Print GHC version to snapshot mapping"))+ <> command "info" (info (withColorOption infoCmdParser) (progDesc "Print repository path and GHC version to snapshot mapping")) <> command "config" (info (withColorOption configParser) (progDesc "Configure stacker")) <> command "version" (info (withColorOption (pure Version)) (progDesc "Print version information (also: -V, --version)")) <> command "numeric-version" (info (withColorOption (pure NumericVersion)) (progDesc "Print version number (also: --numeric-version)")) <> command "license" (info (withColorOption (pure PrintLicense)) (progDesc "Print license text (also: --license)")) <> command "help" (info (withColorOption (pure Help)) (progDesc "Print this help (also: -h, --help)")) )- + -- Top-level parser for version/help flags and default command when no subcommand is specified topLevelOptions = Options <$> commandParserTopLevel <*> colorOption- + -- Top-level command parser (for flags)- commandParserTopLevel = + commandParserTopLevel = flag' Version (long "version" <> short 'V' <> help "Print version information") <|> flag' NumericVersion (long "numeric-version" <> help "Print version number") <|> flag' PrintLicense (long "license" <> help "Print license text")@@ -69,13 +69,20 @@ ( command "repo" (info repoParser (progDesc "Set repository path")) ) +-- | Info command parser+infoCmdParser :: Parser Command+infoCmdParser = Info <$> (subparser+ ( command "repo" (info (pure InfoRepo) (progDesc "Print repository path"))+ <> command "snapshots" (info (pure InfoSnapshots) (progDesc "Print GHC version to snapshot mapping"))+ ) <|> pure InfoAll)+ -- | Repo config parser repoParser :: Parser ConfigCmd repoParser = SetRepo <$> argument str (metavar "PATH") -- | Helper to parse file arguments with optional recursive flag filesParser :: ([FilePath] -> Bool -> Command) -> Parser Command-filesParser cmd = cmd +filesParser cmd = cmd <$> many (argument str (metavar "FILES..." <> action "file")) <*> recursiveOption
src/PathUtil.hs view
@@ -1,6 +1,6 @@ -- | Path utilities for cross-platform compatibility--- --- This module provides path utilities that use forward slash (/) as the +--+-- This module provides path utilities that use forward slash (/) as the -- path separator on all platforms, including Windows. While Windows primarily -- uses backslash (\), it also accepts forward slash for relative paths, -- which is what this tool deals with.
src/StackYaml.hs view
@@ -18,7 +18,7 @@ import Data.Text.IO qualified as TIO import System.Directory (listDirectory, doesFileExist, pathIsSymbolicLink, getSymbolicLinkTarget, doesDirectoryExist) import PathUtil ((</>), normalizeFilePath, collapseDots, takeDirectory, takeFileName, toForwardSlashes)-import Types (Action(..))+import Types (Action(..), Bump(..)) -- | Check if a filename is a stack*.yaml file isStackYaml :: FilePath -> Bool@@ -43,17 +43,17 @@ entries <- listDirectory dir -- Use custom </> to ensure consistent path output with "/" separator on all platforms let fullPaths = map (dir </>) entries- + -- Process files and directories separately files <- filterM doesFileExist fullPaths dirs <- filterM doesDirectoryExist fullPaths- + -- Find stack*.yaml files in current directory, normalize to remove "./" prefix let stackYamls = map normalizeFilePath $ filter isStackYaml files- + -- Recursively search subdirectories subResults <- mapM findStackYamlFilesInDir dirs- + -- Combine and sort all results (all paths are already normalized) return $ sort (stackYamls ++ concat subResults) @@ -122,9 +122,10 @@ case actionSymlinkTarget action of Just _ -> return () -- Skip symlinks Nothing ->- case actionNewSnapshot action of- Nothing -> return () -- No update needed- Just newSnap -> do+ case actionBump action of+ UpToDate -> return ()+ Unknown -> return ()+ BumpTo newSnap -> do when verbose $ putStrLn $ "Updating " ++ actionFile action content <- TIO.readFile (actionFile action) let (before, after) = splitAtSpan (actionSpan action) content
src/Types.hs view
@@ -34,11 +34,18 @@ | Nightly NightlyVersion deriving (Eq, Ord, Show, Generic) +-- | Outcome of analyzing a snapshot+data Bump+ = UpToDate -- ^ Already the latest snapshot for its GHC major version+ | BumpTo Text -- ^ Should be bumped to this snapshot+ | Unknown -- ^ Snapshot not recognized (e.g. not in the database)+ deriving (Eq, Show, Generic)+ -- | Action to take for a stack*.yaml file data Action = Action { actionFile :: FilePath , actionOldSnapshot :: Text- , actionNewSnapshot :: Maybe Text+ , actionBump :: Bump , actionIsResolver :: Bool -- True if 'resolver' field, False if 'snapshot' , actionSpan :: (Int, Int) -- Character span to replace , actionSymlinkTarget :: Maybe FilePath -- Just target if this is a symlink to another stack*.yaml in the list@@ -62,11 +69,18 @@ | DryRun [FilePath] Bool -- Optional list of files to check, recursive flag | Config ConfigCmd | Update- | Info+ | Info InfoCmd | Version | NumericVersion | PrintLicense | Help+ deriving (Eq, Show)++-- | Info command+data InfoCmd+ = InfoAll -- ^ Print repo path and snapshots (in YAML format)+ | InfoRepo -- ^ Print just the repo path+ | InfoSnapshots -- ^ Print just the GHC version to snapshot mapping deriving (Eq, Show) -- | Config command
stacker.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: stacker-version: 0.0.0.0+version: 0.0.1.0 synopsis: Bump snapshots in stack.yaml files description: A tool to bump snapshots (resolvers) in stack*.yaml files.@@ -19,6 +19,7 @@ GHC == 9.8.4 GHC == 9.10.3 GHC == 9.12.4+ GHC == 9.14.1 extra-doc-files: CHANGELOG.md@@ -84,6 +85,7 @@ Paths_stacker build-depends: , base >= 4.17 && < 5+ , bytestring >= 0.11 , directory >= 1.3.7.0 -- XdgState requires directory-1.3.7.0 or higher , filepath >= 1.4@@ -110,6 +112,7 @@ hs-source-dirs: test main-is: Main.hs other-modules:+ TestAnalysis TestDryRun TestBump build-depends:@@ -117,8 +120,10 @@ , stacker , tasty >= 1.4 , tasty-golden >= 2.3+ , tasty-hunit >= 0.10 , directory >= 1.3.7.0 , text >= 2.0+ , containers >= 0.6 , bytestring >= 0.11 , temporary >= 1.3 , process
test/Main.hs view
@@ -1,6 +1,7 @@ module Main where import Test.Tasty ( defaultMain, testGroup, TestTree )+import TestAnalysis ( analysisTests ) import TestDryRun ( dryRunTests ) import TestBump ( bumpTestsIO ) @@ -11,6 +12,7 @@ tests :: [TestTree] -> TestTree tests bumpTests = testGroup "stack-snapshots tests"- [ testGroup "dry-run tests" dryRunTests+ [ testGroup "analysis tests" analysisTests+ , testGroup "dry-run tests" dryRunTests , testGroup "bump tests" bumpTests ]
+ test/TestAnalysis.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE OverloadedStrings #-}++module TestAnalysis (analysisTests) where++import Data.Map.Strict qualified as Map+import Test.Tasty (TestTree)+import Test.Tasty.HUnit (testCase, (@?=))++import Analysis (determineNewSnapshot)+import Types++-- | A small, fixed snapshot database, independent of the user's state directory.+testDB :: SnapshotDB+testDB = SnapshotDB+ { dbLTS = Map.fromList+ [ (LTSVersion 24 1, GHCVersion 9 10 2)+ , (LTSVersion 24 5, GHCVersion 9 10 3)+ ]+ , dbNightly = Map.fromList+ [ (NightlyVersion 2025 5 9, GHCVersion 9 10 2)+ , (NightlyVersion 2026 8 27, GHCVersion 9 12 4)+ -- No nightly for 2026-08-28 and 2026-08-29.+ , (NightlyVersion 2026 8 30, GHCVersion 9 12 4)+ , (NightlyVersion 2026 10 12, GHCVersion 9 12 4)+ ]+ , dbGHC = Map.empty+ }++analysisTests :: [TestTree]+analysisTests =+ [ testCase "nightly with LTS for its GHC bumps to LTS" $+ determineNewSnapshot testDB "nightly-2025-05-09" @?= BumpTo "lts-24.5"+ , testCase "known nightly bumps to latest nightly" $+ determineNewSnapshot testDB "nightly-2026-08-27" @?= BumpTo "nightly-2026-10-12"+ , testCase "nightly missing from database bumps to latest nightly" $+ determineNewSnapshot testDB "nightly-2026-08-28" @?= BumpTo "nightly-2026-10-12"+ , testCase "latest nightly is up to date" $+ determineNewSnapshot testDB "nightly-2026-10-12" @?= UpToDate+ , testCase "nightly newer than database is unknown" $+ determineNewSnapshot testDB "nightly-2026-10-20" @?= Unknown+ , testCase "LTS bumps to latest minor" $+ determineNewSnapshot testDB "lts-24.1" @?= BumpTo "lts-24.5"+ , testCase "latest LTS is up to date" $+ determineNewSnapshot testDB "lts-24.5" @?= UpToDate+ , testCase "LTS newer than database is unknown" $+ determineNewSnapshot testDB "lts-24.9" @?= Unknown+ , testCase "LTS with unknown major is unknown" $+ determineNewSnapshot testDB "lts-99.1" @?= Unknown+ , testCase "non-Stackage resolver is unknown" $+ determineNewSnapshot testDB "ghc-9.12.4" @?= Unknown+ ]
test/golden/dry-run-recursive.golden view
@@ -1,6 +1,6 @@-other/stack.yaml lts-24.20 → bump to lts-24.36+other/stack.yaml lts-24.20 → bump to lts-24.63 stack-9.6.yaml lts-22.40 → bump to lts-22.44 stack.yaml lts-22.40 = symlink to stack-9.6.yaml subdir1/stack-9.8.yaml lts-23.20 → bump to lts-23.28 subdir1/stack-other.yaml lts-24.20 = symlink to other/stack.yaml-subdir1/subdir2/stack-9.10.yaml nightly-2025-01-01 → bump to lts-24.36+subdir1/subdir2/stack-9.10.yaml nightly-2025-01-01 → bump to lts-24.63
test/golden/dry-run.golden view
@@ -1,6 +1,6 @@ stack-9.0.yaml nightly-2021-12-04 → bump to lts-19.33-stack-9.10.yaml nightly-2025-05-14 → bump to lts-24.36-stack-9.12.yaml nightly-2025-10-05 → bump to nightly-2026-04-07+stack-9.10.yaml nightly-2025-05-14 → bump to lts-24.63+stack-9.12.yaml nightly-2025-10-05 → bump to nightly-2026-10-12 stack-9.2.yaml lts-20.1 → bump to lts-20.26 stack-9.4.yaml lts-21.20 → bump to lts-21.25 stack-9.6.yaml nightly-2023-08-04 → bump to lts-22.44
test/golden/tests/stack-9.10.yaml view
@@ -1,4 +1,4 @@-resolver: lts-24.36+resolver: lts-24.63 packages: - lib/
test/golden/tests/stack-9.12.yaml view
@@ -1,1 +1,1 @@-resolver: nightly-2026-04-07+resolver: nightly-2026-10-12
test/golden/tests/stack.yaml view
@@ -1,4 +1,4 @@-resolver: lts-24.36+resolver: lts-24.63 packages: - lib/