diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2015, Oleg Grenrus
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Oleg Grenrus nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,64 @@
+# travis-meta-yaml
+
+[![Build Status](https://travis-ci.org/phadej/travis-meta-yaml.svg?branch=master)](https://travis-ci.org/phadej/travis-meta-yaml)
+[![Hackage](https://img.shields.io/hackage/v/travis-meta-yaml.svg)](http://hackage.haskell.org/package/travis-meta-yaml)
+
+## Motivation
+
+Turn
+
+```yaml
+env:
+  - CABALVER=1.18 GHCVER=7.8.4
+  - CABALVER=1.22 GHCVER=7.10.1
+  - CABALVER=1.22 GHCVER=head
+
+addons:
+  apt:
+    sources:
+      - hvr-ghc
+    packages:
+      - cabal-install-$CABALVER
+      - ghc-$GHCVER
+
+matrix:
+  allow_failures:
+    - env: CABALVER=1.22 GHCVER=head
+```
+
+into
+
+```yaml
+matrix:
+  include:
+  - env: CABALVER=1.18 GHCVER=7.8.4
+    addons:
+      apt:
+        sources:
+        - hvr-ghc
+        packages:
+        - cabal-install-1.18
+        - ghc-7.8.4
+  - env: CABALVER=1.22 GHCVER=7.10.1
+    addons:
+      apt:
+        sources:
+        - hvr-ghc
+        packages:
+        - cabal-install-1.22
+        - ghc-7.10.1
+  - env: CABALVER=1.22 GHCVER=head
+    addons:
+      apt:
+        sources:
+        - hvr-ghc
+        packages:
+        - cabal-install-1.22
+        - ghc-head
+  allow_failures:
+  - env: CABALVER=1.22 GHCVER=head
+```
+
+## Features
+
+Currently only expanding of `addons.apt`. Any feature requests are welcome.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/cli/Main.hs b/cli/Main.hs
new file mode 100644
--- /dev/null
+++ b/cli/Main.hs
@@ -0,0 +1,30 @@
+module Main (main) where
+
+import Travis.Meta
+import Options.Applicative
+
+data Opts = Opts
+  { _source :: FilePath
+  , _target :: FilePath
+  }
+  deriving (Eq, Show)
+
+sample :: Parser Opts
+sample = Opts
+  <$> strArgument
+      ( metavar "SOURCE"
+     <> value ".travis.meta.yml"
+     <> showDefault
+     <> help "Source, travis meta file" )
+  <*> strArgument
+      ( metavar "TARGET"
+     <> value ".travis.yml"
+     <> showDefault
+     <> help "Target, travis yaml" )
+
+main :: IO ()
+main = execParser opts >>= \(Opts source target) -> preprocessIO source target
+  where
+    opts = info (helper <*> sample)
+      ( fullDesc
+     <> header "travis-meta-yaml - .travis.yml preprocessor" )
diff --git a/fixtures/matrix-include.yml b/fixtures/matrix-include.yml
new file mode 100644
--- /dev/null
+++ b/fixtures/matrix-include.yml
@@ -0,0 +1,4 @@
+matrix:
+  include:
+    - env: FOO=foo
+    - env: FOO=bar
diff --git a/fixtures/without-matrix-processed.yml b/fixtures/without-matrix-processed.yml
new file mode 100644
--- /dev/null
+++ b/fixtures/without-matrix-processed.yml
@@ -0,0 +1,39 @@
+script:
+- cabal configure --enable-tests
+- cabal test -j2
+matrix:
+  include:
+  - env: CABALVER=1.18 GHCVER=7.8.4
+    addons:
+      apt:
+        sources:
+        - hvr-ghc
+        packages:
+        - cabal-1.18
+        - ghc-7.8.4
+  - env: CABALVER=1.22 GHCVER=7.10.1
+    addons:
+      apt:
+        sources:
+        - hvr-ghc
+        packages:
+        - cabal-1.22
+        - ghc-7.10.1
+  - env: CABALVER=1.22 GHCVER=head
+    addons:
+      apt:
+        sources:
+        - hvr-ghc
+        packages:
+        - cabal-1.22
+        - ghc-head
+install:
+- cabal install -j2 --only-dependencies --enable-tests
+before_install:
+- export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH
+- stack +RTS -N1 -RTS --version
+- echo "$(ghc --version) [$(ghc --print-project-git-commit-id 2> /dev/null || echo
+  '?')]"
+- cabal --version
+- cabal install -j2 --only-dependencies --enable-tests
+sudo: false
diff --git a/fixtures/without-matrix.yml b/fixtures/without-matrix.yml
new file mode 100644
--- /dev/null
+++ b/fixtures/without-matrix.yml
@@ -0,0 +1,30 @@
+sudo: false
+
+env:
+  - CABALVER=1.18 GHCVER=7.8.4
+  - CABALVER=1.22 GHCVER=7.10.1
+  - CABALVER=1.22 GHCVER=head
+
+addons:
+  apt:
+    sources:
+      - hvr-ghc
+    packages:
+      - cabal-$CABALVER
+      - ghc-$GHCVER
+
+before_install:
+  # ghc
+  - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH
+  # versions
+  - stack +RTS -N1 -RTS --version
+  - echo "$(ghc --version) [$(ghc --print-project-git-commit-id 2> /dev/null || echo '?')]"
+  - cabal --version
+  - cabal install -j2 --only-dependencies --enable-tests
+
+install:
+  - cabal install -j2 --only-dependencies --enable-tests
+
+script:
+  - cabal configure --enable-tests
+  - cabal test -j2
diff --git a/src/Travis/Meta.hs b/src/Travis/Meta.hs
new file mode 100644
--- /dev/null
+++ b/src/Travis/Meta.hs
@@ -0,0 +1,105 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+module Travis.Meta (
+  -- * High level
+    preprocessIO
+  , preprocess
+  , preprocessYaml
+  -- * Internal
+  , Env
+  , parseEnv
+  , interpolateEnv
+  ) where
+
+import Control.Lens
+import Control.Monad hiding (sequence)
+import Data.Aeson.Types
+import Data.Aeson.Lens
+import Data.ByteString as BS
+import Data.Char
+import Data.Maybe
+import Data.Monoid
+import Data.Text as T
+import Data.Traversable
+import Data.Yaml
+import Prelude hiding (sequence)
+import Text.Regex.Applicative
+
+type Env = [(Text, Text)]
+
+-- | Parse environment string.
+--
+-- > >>> parseEnv "CABALVER=1.18 GHCVER=7.8.4"
+-- > Right [("CABALVER","1.18"),("GHCVER","7.8.4")]
+parseEnv :: Text -> Either String Env
+parseEnv = traverse (f . T.splitOn "=") . T.words
+  where f [k, n] = Right (k, n)
+        f _      = Left "Cannot parse"
+
+-- > match (interpolationRe $ flip lookup [("foo", "bar")]) "$foo"
+-- Just (Just "bar")
+interpolationRe :: (String -> Maybe String) -> RE Char (Maybe String)
+interpolationRe l = comb <$> many (interpolationChar l)
+  where comb :: [Maybe [a]] -> Maybe [a]
+        comb = fmap Prelude.concat . sequence
+
+interpolationChar :: (String -> Maybe String) -> RE Char (Maybe String)
+interpolationChar l = var <|> other
+  where var = l <$ sym '$' <*> many (psym isAlpha)
+        other = (\x -> Just [x]) <$> anySym
+
+-- | Interpolate env. Substitute all @$VAR@ occurrences with values from 'Env'.
+-- If variable is not in the environment, return 'Nothing'.
+--
+-- > >>> interpolateEnv [("FOO", "foo")] "res-$FOO-bar"
+-- > Just "res-foo-bar"
+--
+-- > >>> interpolateEnv [("FOO","foo")] "yes-$FOOBAR-$FOO"
+-- > Nothing
+interpolateEnv :: Env -> Text -> Either String Text
+interpolateEnv env = hoist . fmap T.pack . join . match (interpolationRe l) . T.unpack
+  where l = fmap T.unpack . flip lookup env . T.pack
+
+hoist :: Maybe a -> Either String a
+hoist Nothing = Left "error in interpolation"
+hoist (Just a) = Right a
+
+preprocessYaml :: Value -> Either String Value
+preprocessYaml v = do
+  assertNoMatrixInclude v
+  matrixInclude <- buildMatrixInclude v
+  let v' = v & _Object . at "env" .~ Nothing
+             & _Object . at "addons" .~ Nothing
+             & _Object . at "matrix" ?~ (fromMaybe (Object mempty) (v ^? key "matrix"))
+             & key "matrix" . _Object . at "include" ?~ matrixInclude
+  return v'
+
+buildMatrixInclude :: Value -> Either String Value
+buildMatrixInclude v = toJSON <$> mk `traverse` envs
+  where addons  = v ^? key "addons"
+        envs    = v ^.. key "env" . values . _String
+        mk env  = do env' <- parseEnv env
+                     addons' <- traverseOf (deep _String) (interpolateEnv env') `traverse` addons
+                     case addons' of
+                       Just addons'' -> return $ object [ "env" Data.Yaml..= env, "addons" Data.Yaml..= addons'' ]
+                       Nothing       -> return $ object [ "env" Data.Yaml..= env ]
+                     
+
+assertNoMatrixInclude :: Value -> Either String ()
+assertNoMatrixInclude v =
+  case v ^? key "matrix" . key "include" of
+    Nothing -> Right ()
+    Just v' -> Left $ "matrix.include specified: " ++ show v'
+
+preprocess :: ByteString -> Either String ByteString
+preprocess = fmap encode . preprocessYaml <=< decodeEither
+
+preprocessIO :: FilePath -> FilePath -> IO ()
+preprocessIO source target = do
+  contents <- BS.readFile source
+  case preprocess contents of
+    Left err -> error err
+    Right bs -> BS.writeFile target bs
+
+-- Right v <- decodeEither <$> BS.readFile ".travis.meta.yml"  :: IO (Either String Value)
+-- BS.putStr $ encode $ preprocessYaml v
diff --git a/tests/Tests.hs b/tests/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/Tests.hs
@@ -0,0 +1,37 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE RankNTypes #-}
+module Main (main) where
+
+import Data.Either.Extra
+import Data.FileEmbed
+import Data.Yaml
+import Test.Tasty
+import Test.Tasty.QuickCheck
+
+import Travis.Meta
+
+main :: IO ()
+main = Test.Tasty.defaultMain tests
+
+tests :: TestTree
+tests = testGroup "Tests"
+  [ selfTest
+  , withoutMatrix
+  , errorCases
+  ]
+
+selfTest :: TestTree
+selfTest = testProperty "Self test" $ once $ actual === processed
+  where actual     = decodeEither $(embedFile ".travis.yml")
+        processed  = preprocessYaml =<< decodeEither $(embedFile ".travis.meta.yml")
+
+withoutMatrix :: TestTree
+withoutMatrix = testProperty "Self test without matrix" $ once $ actual === processed
+  where actual     = decodeEither $(embedFile "fixtures/without-matrix-processed.yml")
+        processed  = preprocessYaml =<< decodeEither $(embedFile "fixtures/without-matrix.yml")
+
+errorCases :: TestTree
+errorCases = testGroup "Error cases"
+  [ c "matrix.include specified" $(embedFile "fixtures/matrix-include.yml")
+  ]
+  where c name bs = testProperty name $ once $ isLeft $ preprocess bs
diff --git a/travis-meta-yaml.cabal b/travis-meta-yaml.cabal
new file mode 100644
--- /dev/null
+++ b/travis-meta-yaml.cabal
@@ -0,0 +1,84 @@
+-- This file has been generated from package.yaml by hpack version 0.5.2.
+--
+-- see: https://github.com/sol/hpack
+
+name:           travis-meta-yaml
+version:        0.0.1.0
+synopsis:       .travis.yml preprocessor
+description:    .travis.yml preprocessor
+category:       Development
+homepage:       https://github.com/phadej/travis-meta-yaml#readme
+bug-reports:    https://github.com/phadej/travis-meta-yaml/issues
+author:         Oleg Grenrus <oleg.grenrus@iki.fi>
+maintainer:     Oleg Grenrus <oleg.grenrus@iki.fi>
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+cabal-version:  >= 1.10
+
+extra-source-files:
+    fixtures/matrix-include.yml
+    fixtures/without-matrix-processed.yml
+    fixtures/without-matrix.yml
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/phadej/travis-meta-yaml
+
+library
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  build-depends:
+      base >=4.7 && <4.9
+    , aeson
+    , bytestring
+    , lens-aeson
+    , lens
+    , text
+    , regex-applicative
+    , yaml
+  exposed-modules:
+      Travis.Meta
+  default-language: Haskell2010
+
+executable travis-meta-yaml
+  main-is: Main.hs
+  hs-source-dirs:
+      cli
+  ghc-options: -Wall
+  build-depends:
+      base >=4.7 && <4.9
+    , aeson
+    , bytestring
+    , lens-aeson
+    , lens
+    , text
+    , regex-applicative
+    , yaml
+    , travis-meta-yaml
+    , optparse-applicative
+  default-language: Haskell2010
+
+test-suite tests
+  type: exitcode-stdio-1.0
+  main-is: Tests.hs
+  hs-source-dirs:
+      tests
+  ghc-options: -Wall
+  build-depends:
+      base >=4.7 && <4.9
+    , aeson
+    , bytestring
+    , lens-aeson
+    , lens
+    , text
+    , regex-applicative
+    , yaml
+    , extra
+    , file-embed
+    , travis-meta-yaml
+    , tasty
+    , tasty-quickcheck
+  default-language: Haskell2010
