From 8a51daf280ebb252ef51a979e3f18dd5be84c6c9 Mon Sep 17 00:00:00 2001
From: Gleb Mazovetskiy <glex.spb@gmail.com>
Date: Mon, 15 Dec 2014 18:18:27 +0000
Subject: [PATCH] Compile with Ruby Sass or libsass.

* Separate configs for libsass and sass.
* Sass compiler selected based on `process.env.TWBS_SASS`.
* Travis:
  * Use Gemfile to manage ruby dependencies.
  * Run core tests with both Sass compilers.
  * Only install/cache ruby gems required by the test subset.
* Grunt: `update-gemfile-lock` task a la `update-shrinkwrap`.
---
 .gitignore                            |  8 ++-
 .travis.yml                           | 12 ++---
 Gemfile                               |  7 ++-
 Gemfile.lock                          | 78 +++++++++++++++++++++++++++
 Gruntfile.js                          | 43 +++++++--------
 grunt/bs-sass-compile/libsass.js      | 26 +++++++++
 grunt/bs-sass-compile/sass.js         | 30 +++++++++++
 package.json                          |  1 +
 test-infra/S3Cachefile.json           |  6 +--
 test-infra/gemfiles/core.gemfile      |  7 +++
 test-infra/gemfiles/core.gemfile.lock | 15 ++++++
 11 files changed, 200 insertions(+), 33 deletions(-)
 create mode 100644 Gemfile.lock
 create mode 100644 grunt/bs-sass-compile/libsass.js
 create mode 100644 grunt/bs-sass-compile/sass.js
 create mode 100644 test-infra/gemfiles/core.gemfile
 create mode 100644 test-infra/gemfiles/core.gemfile.lock

diff --git a/.gitignore b/.gitignore
index d60e71a5fd..042d47047f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,11 @@
 # Ignore docs files
 _gh_pages
 _site
+
+# Ignore ruby files
 .ruby-version
-Gemfile.lock
+.bundle
+vendor/cache
 
 # Numerous always-ignore extensions
 *.diff
@@ -41,6 +44,9 @@ validation-status.json
 # SCSS-Lint
 scss-lint-report.xml
 
+# grunt-contrib-sass cache
+.sass-cache
+
 # Folders to ignore
 bower_components
 node_modules
diff --git a/.travis.yml b/.travis.yml
index 5899bbf0f7..bb85115f61 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,15 +15,12 @@ before_install:
 install:
   - npm install -g grunt-cli
   - ./test-infra/s3_cache.py download npm-modules
-  - if [ "$TWBS_TEST" = validate-html ] && [ $TWBS_DO_VALIDATOR -ne 0 ]; then ./test-infra/s3_cache.py download rubygems; fi
+  - if [ -n "$BUNDLE_GEMFILE" ]; then ./test-infra/s3_cache.py download rubygems; fi
 after_script:
   - if [ "$TRAVIS_REPO_SLUG" != twbs-savage/bootstrap ] && [ "$TWBS_TEST" = core ]; then ./test-infra/s3_cache.py upload npm-modules; fi
-  - if [ "$TRAVIS_REPO_SLUG" != twbs-savage/bootstrap ] && [ "$TWBS_TEST" = validate-html ] && [ $TWBS_DO_VALIDATOR -ne 0 ]; then ./test-infra/s3_cache.py upload rubygems; fi
+  - if [ "$TRAVIS_REPO_SLUG" != twbs-savage/bootstrap ] && [ -n "$BUNDLE_GEMFILE" ]; then ./test-infra/s3_cache.py upload rubygems; fi
 env:
   global:
-    - JEKYLL_VERSION="2.5.1"
-    - ROUGE_VERSION="1.7.2"
-    - SCSS_LINT_VERSION="0.30.0"
     - SAUCE_USERNAME="bootstrap"
     - secure: "pJkBwnuae9dKU5tEcCqccfS1QQw7/meEcfz63fM7ba7QJNjoA6BaXj08L5Z3Vb5vBmVPwBawxo5Hp0jC0r/Z/O0hGnAmz/Cz09L+cy7dSAZ9x4hvZePSja/UAusaB5ogMoO8l2b773MzgQeSmrLbExr9BWLeqEfjC2hFgdgHLaQ="
     - secure: "gqjqISbxBJK6byFbsmr1AyP1qoWH+rap06A2gI7v72+Tn2PU2nYkIMUkCvhZw6K889jv+LhQ/ybcBxDOXHpNCExCnSgB4dcnmYp+9oeNZb37jSP0rQ+Ib4OTLjzc3/FawE/fUq5kukZTC7porzc/k0qJNLAZRx3YLALmK1GIdUY="
@@ -35,8 +32,9 @@ env:
     - secure: "PabpUdG2dE40hHUkMCdxk1e9Ak3BOo0h7Y5/uekosLKOz5N60Xmn/ooyrSkvicLthXO4cfONFhO3/xSVRKQOxlUw4on5i0VuNK+QSqxJk0IDaRSZnTCcC8J7083K0YL+FvMdGQwcYwMY9LiwS8aS014IRkSQjsa+mjo3owP+dOU="
     - secure: "G4/f4PVyVi9o6UbZMqw9YFmDu7cHqe9iymiXYd1RcnPXwhWAePX12m0PWMhUj5itJ180PTEddVip8PNOgBdqyrDxEPKkcgAW2EElVAPIKJXVfvDW64UjQ0H7NS7XvF7iLQUJp/XfmR7NJ7tT393AQdh8SGmuQpJhgYbwIWbES/k="
   matrix:
-    - TWBS_TEST=core
-    - TWBS_TEST=validate-html
+    - TWBS_TEST=core TWBS_SASS=libsass BUNDLE_GEMFILE=test-infra/gemfiles/core.gemfile
+    - TWBS_TEST=core TWBS_SASS=sass    BUNDLE_GEMFILE=test-infra/gemfiles/core.gemfile
+    - TWBS_TEST=validate-html BUNDLE_GEMFILE=Gemfile
     - TWBS_TEST=sauce-js-unit
 matrix:
   fast_finish: true
diff --git a/Gemfile b/Gemfile
index eb9646efef..23068bdfeb 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,11 @@
+# Ruby Gems for building and testing Bootstrap
+# Run `grunt update-gemfile-lock` to update to the latest compatible versions
+
 source 'https://rubygems.org'
 
-group :development do
+group :development, :test do
   gem 'jekyll', '~> 2.5.2'
   gem 'rouge', '~> 1.7.4'
+  gem 'sass', '~> 3.4.9'
+  gem 'scss-lint', '~> 0.31'
 end
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000000..725379399c
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,78 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    blankslate (2.1.2.4)
+    celluloid (0.16.0)
+      timers (~> 4.0.0)
+    classifier-reborn (2.0.2)
+      fast-stemmer (~> 1.0)
+    coffee-script (2.3.0)
+      coffee-script-source
+      execjs
+    coffee-script-source (1.8.0)
+    colorator (0.1)
+    execjs (2.2.2)
+    fast-stemmer (1.0.2)
+    ffi (1.9.6)
+    hitimes (1.2.2)
+    jekyll (2.5.2)
+      classifier-reborn (~> 2.0)
+      colorator (~> 0.1)
+      jekyll-coffeescript (~> 1.0)
+      jekyll-gist (~> 1.0)
+      jekyll-paginate (~> 1.0)
+      jekyll-sass-converter (~> 1.0)
+      jekyll-watch (~> 1.1)
+      kramdown (~> 1.3)
+      liquid (~> 2.6.1)
+      mercenary (~> 0.3.3)
+      pygments.rb (~> 0.6.0)
+      redcarpet (~> 3.1)
+      safe_yaml (~> 1.0)
+      toml (~> 0.1.0)
+    jekyll-coffeescript (1.0.1)
+      coffee-script (~> 2.2)
+    jekyll-gist (1.1.0)
+    jekyll-paginate (1.1.0)
+    jekyll-sass-converter (1.3.0)
+      sass (~> 3.2)
+    jekyll-watch (1.2.0)
+      listen (~> 2.7)
+    kramdown (1.5.0)
+    liquid (2.6.1)
+    listen (2.8.4)
+      celluloid (>= 0.15.2)
+      rb-fsevent (>= 0.9.3)
+      rb-inotify (>= 0.9)
+    mercenary (0.3.5)
+    parslet (1.5.0)
+      blankslate (~> 2.0)
+    posix-spawn (0.3.9)
+    pygments.rb (0.6.0)
+      posix-spawn (~> 0.3.6)
+      yajl-ruby (~> 1.1.0)
+    rainbow (2.0.0)
+    rb-fsevent (0.9.4)
+    rb-inotify (0.9.5)
+      ffi (>= 0.5.0)
+    redcarpet (3.2.2)
+    rouge (1.7.4)
+    safe_yaml (1.0.4)
+    sass (3.4.9)
+    scss-lint (0.31.0)
+      rainbow (~> 2.0)
+      sass (~> 3.4.1)
+    timers (4.0.1)
+      hitimes
+    toml (0.1.2)
+      parslet (~> 1.5.0)
+    yajl-ruby (1.1.0)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  jekyll (~> 2.5.2)
+  rouge (~> 1.7.4)
+  sass (~> 3.4.9)
+  scss-lint (~> 0.31)
diff --git a/Gruntfile.js b/Gruntfile.js
index d09e6f5606..582f4ac308 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -17,6 +17,7 @@ module.exports = function (grunt) {
 
   var fs = require('fs');
   var path = require('path');
+  var glob = require('glob');
   var npmShrinkwrap = require('npm-shrinkwrap');
 
   var generateCommonJSModule = require('./grunt/bs-commonjs-generator.js');
@@ -144,25 +145,6 @@ module.exports = function (grunt) {
       files: 'js/tests/index.html'
     },
 
-    sass: {
-      options: {
-        includePaths: ['scss'],
-        precision: 6,
-        sourceComments: false,
-        sourceMap: true
-      },
-      core: {
-        files: {
-          'dist/css/<%= pkg.name %>.css': 'scss/<%= pkg.name %>.scss'
-        }
-      },
-      docs: {
-        files: {
-          'docs/assets/css/docs.min.css': 'docs/assets/scss/docs.scss'
-        }
-      }
-    },
-
     scsslint: {
       scss: ['scss/*.scss', '!scss/_normalize.scss'],
       options: {
@@ -342,13 +324,23 @@ module.exports = function (grunt) {
     exec: {
       npmUpdate: {
         command: 'npm update'
+      },
+      bundleUpdate: {
+        command: function () {
+          // Update dev gems and all the test gemsets
+          return 'bundle update && ' + glob.sync('test-infra/gemfiles/*.gemfile').map(function (gemfile) {
+            return 'BUNDLE_GEMFILE=' + gemfile + ' bundle update';
+          }).join(' && ');
+        }
       }
     }
   });
 
 
   // These plugins provide necessary tasks.
-  require('load-grunt-tasks')(grunt, { scope: 'devDependencies' });
+  require('load-grunt-tasks')(grunt, { scope: 'devDependencies',
+    // Exclude Sass compilers. We choose the one to load later on.
+    pattern: ['grunt-*', '!grunt-sass', '!grunt-contrib-sass'] });
   require('time-grunt')(grunt);
 
   // Docs HTML validation task
@@ -366,7 +358,8 @@ module.exports = function (grunt) {
   // Skip core tests if running a different subset of the test suite
   if (runSubset('core') &&
     // Skip core tests if this is a Savage build
-    process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'test-scss', 'test-js', 'docs']);
+    process.env.TRAVIS_REPO_SLUG !== 'twbs-savage/bootstrap') {
+    testSubtasks = testSubtasks.concat(['dist-css', 'dist-js', 'test-scss', 'test-js', 'docs']);
   }
   // Skip HTML validation if running a different subset of the test suite
   if (runSubset('validate-html') &&
@@ -392,7 +385,12 @@ module.exports = function (grunt) {
   grunt.registerTask('test-scss', ['scsslint:scss']);
 
   // CSS distribution task.
+  // Supported Compilers: sass (Ruby) and libsass.
+  (function (sassCompilerName) {
+    require('./grunt/bs-sass-compile/' + sassCompilerName + '.js')(grunt);
+  })(process.env.TWBS_SASS || 'libsass');
   grunt.registerTask('sass-compile', ['sass:core', 'sass:docs']);
+
   grunt.registerTask('dist-css', ['sass-compile', 'autoprefixer:core', 'usebanner', 'csscomb:dist', 'cssmin:core', 'cssmin:docs']);
 
   // Full distribution task.
@@ -435,4 +433,7 @@ module.exports = function (grunt) {
       done();
     });
   });
+  // Task for updating the cached RubyGem packages used by the Travis build (which are controlled by test-infra/Gemfile.lock).
+  // This task should be run and the updated file should be committed whenever Bootstrap's RubyGem dependencies change.
+  grunt.registerTask('update-gemfile-lock', ['exec:bundleUpdate']);
 };
diff --git a/grunt/bs-sass-compile/libsass.js b/grunt/bs-sass-compile/libsass.js
new file mode 100644
index 0000000000..e3d13a989f
--- /dev/null
+++ b/grunt/bs-sass-compile/libsass.js
@@ -0,0 +1,26 @@
+// Compile Bootstrap with [libsass][1] using [grunt-sass][2]
+// [1]: https://github.com/sass/libsass
+// [2]: https://github.com/sindresorhus/grunt-sass
+module.exports = function configureLibsass(grunt) {
+  grunt.config.merge({
+    sass: {
+      options: {
+        includePaths: ['scss'],
+        precision: 6,
+        sourceComments: false,
+        sourceMap: true
+      },
+      core: {
+        files: {
+          'dist/css/<%= pkg.name %>.css': 'scss/<%= pkg.name %>.scss'
+        }
+      },
+      docs: {
+        files: {
+          'docs/assets/css/docs.min.css': 'docs/assets/scss/docs.scss'
+        }
+      }
+    }
+  });
+  grunt.loadNpmTasks('grunt-sass');
+};
diff --git a/grunt/bs-sass-compile/sass.js b/grunt/bs-sass-compile/sass.js
new file mode 100644
index 0000000000..d7743515d8
--- /dev/null
+++ b/grunt/bs-sass-compile/sass.js
@@ -0,0 +1,30 @@
+// Compile Bootstrap with [Ruby Sass][1] using [grunt-contrib-sass][2]
+// [1]: https://github.com/sass/sass
+// [2]: https://github.com/gruntjs/grunt-contrib-sass
+module.exports = function configureRubySass(grunt) {
+  var options = {
+    loadPath: ['scss'],
+    precision: 6,
+    sourcemap: 'auto',
+    style: 'expanded',
+    trace: true,
+    bundleExec: true
+  };
+  grunt.config.merge({
+    sass: {
+      core: {
+        options: options,
+        files: {
+          'dist/css/<%= pkg.name %>.css': 'scss/<%= pkg.name %>.scss'
+        }
+      },
+      docs: {
+        options: options,
+        files: {
+          'docs/assets/css/docs.min.css': 'docs/assets/scss/docs.scss'
+        }
+      }
+    }
+  });
+  grunt.loadNpmTasks('grunt-contrib-sass');
+};
diff --git a/package.json b/package.json
index 049eec9999..8d0502a83e 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
     "grunt-contrib-jade": "~0.13.0",
     "grunt-contrib-jshint": "~0.10.0",
     "grunt-contrib-qunit": "~0.5.2",
+    "grunt-contrib-sass": "^0.8.1",
     "grunt-contrib-uglify": "~0.6.0",
     "grunt-contrib-watch": "~0.6.1",
     "grunt-csscomb": "~3.0.0",
diff --git a/test-infra/S3Cachefile.json b/test-infra/S3Cachefile.json
index 04d8092467..90eae796dc 100644
--- a/test-infra/S3Cachefile.json
+++ b/test-infra/S3Cachefile.json
@@ -5,8 +5,8 @@
         "generate": "./uncached-npm-install.sh"
     },
     "rubygems": {
-        "key": "../pseudo_Gemfile.lock",
-        "cache": "$GEMDIR",
-        "generate": "gem install -N scss-lint -v $SCSS_LINT_VERSION && gem install -N jekyll -v $JEKYLL_VERSION && gem install -N rouge -v $ROUGE_VERSION"
+        "key": "$BUNDLE_GEMFILE",
+        "cache": "../vendor/cache",
+        "generate": "cd .. ; bundle install --path=\"`pwd`/vendor/cache\""
     }
 }
diff --git a/test-infra/gemfiles/core.gemfile b/test-infra/gemfiles/core.gemfile
new file mode 100644
index 0000000000..6a83726919
--- /dev/null
+++ b/test-infra/gemfiles/core.gemfile
@@ -0,0 +1,7 @@
+# Ruby Gems for the 'core' set of tests
+# Run `grunt update-gemfile-lock` to update to the latest compatible versions
+
+source 'https://rubygems.org'
+
+gem 'sass', '~> 3.4.9'
+gem 'scss-lint', '~> 0.31'
diff --git a/test-infra/gemfiles/core.gemfile.lock b/test-infra/gemfiles/core.gemfile.lock
new file mode 100644
index 0000000000..03580d83bb
--- /dev/null
+++ b/test-infra/gemfiles/core.gemfile.lock
@@ -0,0 +1,15 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    rainbow (2.0.0)
+    sass (3.4.9)
+    scss-lint (0.31.0)
+      rainbow (~> 2.0)
+      sass (~> 3.4.1)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  sass (~> 3.4.9)
+  scss-lint (~> 0.31)
-- 
GitLab