<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>lib/sproutcore/build_tools/test_template.rhtml</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -1,3 +1,11 @@
+* Added support for using build numbers in URL strings instead of query string parameters.  This will make caches flush more reliably.
+
+* Added support for building multiple platforms.  This is to be used with mobile platforms.
+
+* Unit tests can now be simple .js files.  These files will be inserted into an html template automatically.
+
+== 0.9.20
+
 == 0.9.19
 
 * back out relative-path change which breaks dev mode [Erich Ocean]</diff>
      <filename>History.txt</filename>
    </modified>
    <modified>
      <diff>@@ -47,6 +47,11 @@ languages = nil
 clean = false
 docs = false
 build_all = false 
+build_numbers = nil
+platforms = nil
+symlink = false
+
+$0 = &quot;sc-build #{ARGV * ' '}&quot;
 
 opts = OptionParser.new do |opts|
   opts.version = SproutCore::VERSION::STRING
@@ -76,7 +81,22 @@ opts = OptionParser.new do |opts|
   opts.on(&quot;-v&quot;, &quot;--[no-]verbose&quot;, &quot;If set, extra debug output will be displayed during the build&quot;) do |opt_verbose|
     verbose = !!opt_verbose
   end
-
+  
+  opts.on(&quot;-p&quot;, &quot;--platform=PLATFORM&quot;, &quot;A comma separated list of the platforms you want to build.  If you do not pass this option, the build tool will build all platforms found in the target bundle.&quot;) do |opt_platform|
+    platforms = opt_platform.split(',').map { |x| x.to_sym }
+  end
+  
+  opts.on(&quot;-b&quot;, &quot;--build=BUILD_NUMBERS&quot;, &quot;build numbers.  Name build number for taret or list of build numbers if needed&quot;) do |opt_build|
+    build_numbers = {}
+    opt_build = opt_build.split(',').map { |x| x.split(':') }.each do |x|
+      if x.size == 1
+        build_numbers[bundle_name.to_sym] = x[0].to_sym
+      elsif x.size &gt; 1
+        build_numbers[x[0].to_sym] = x[1].to_sym
+      end
+    end
+  end
+  
   opts.on(&quot;-L&quot;, &quot;--languages=LANGUAGE&quot;, &quot;Pass a comma separated list of the specific languages you want to build.  If you do not pass this option, the build tool will build all languages required by the bundles to function&quot;) do |opt_lang|
     languages = opt_lang.split(',').map { |x| x.to_sym }
   end
@@ -93,6 +113,10 @@ opts = OptionParser.new do |opts|
     build_all = !!opt_all
   end
 
+  opts.on('-a', '--[no-]symlink', &quot;Use symlinks where possible to reduce build size.  Only use this option if you are both building and deploying on machines that support symlinks&quot;) do |opt_symlink| 
+    symlink = !!opt_symlink
+  end
+  
 end
 opts.parse!
 
@@ -105,7 +129,7 @@ SC.logger.level = (verbose) ? Logger::DEBUG : Logger::INFO
 SC.logger.progname = $0
 
 # Load Library
-library = SC.library_for(library_root, :public_root =&gt; build_root, :build_mode =&gt; build_mode)
+library = SC.library_for(library_root, :public_root =&gt; build_root, :build_mode =&gt; build_mode, :build_numbers =&gt; build_numbers)
 
 if library.nil?
   SC.logger.fatal(&quot;No SproutCore library could be found.  Make sure you are inside of your project directory (the one with an sc-config file in it).&quot;)
@@ -116,13 +140,14 @@ end
 ## FIND BUNDLES TO BUILD
 ##
 
-puts &quot;build_root = #{build_root}&quot;
+target_bundle = [] # save for platform detection, does not include required...
 
 # Find the bundles to build.
 if bundle_name
   # Get bundle
   bundle = library.bundle_for(bundle_name.to_sym)
   not_found(bundle) if bundle.nil?
+  target_bundles = [bundle]
 
   # If dependencies are required, get those bundles as well
   if include_dependencies
@@ -137,6 +162,7 @@ if bundle_name
 else
   bundles = (build_all) ? library.bundles : library.default_bundles_for_build
   SC.logger.info(&quot;Building ALL bundles:\n #{bundles.map { |x| x.bundle_name } * ', '}...&quot;)
+  target_bundles = bundles
 end
 
 ############################################################
@@ -151,7 +177,16 @@ languages ||= library.environment_for(nil)[:build_languages]
 languages = nil unless languages.nil? || languages.size &gt; 0
 languages ||= bundles.map { |b| b.installed_languages }.flatten.uniq 
 
+# Detect required platforms to build
+# Unless platforms are specified on the command line, we build any platforms
+# specified in the build_platforms config or text languages in the target 
+# bundles.
+platforms ||= library.environment_for(nil)[:build_platforms]
+platforms = nil unless platforms.nil? || platforms.size &gt; 0
+platforms ||= target_bundles.map { |b| b.installed_platforms }.flatten.uniq
+
 SC.logger.info(&quot;Building Languages: #{languages.join(', ')}&quot;)
+SC.logger.info(&quot;Building Platforms: #{platforms.join(', ')}&quot;)
 
 SC.logger.info('')
 bundles.each do |bundle|
@@ -162,7 +197,9 @@ bundles.each do |bundle|
     FileUtils.rm_rf(bundle.build_root)
   end
 
-  bundle.build(*languages)
+  SC.logger.info(&quot;~ Build Number: #{bundle.build_number}&quot;)
+  $0 = &quot;sc-build: building #{bundle.bundle_name}&quot;
+  platforms.each { |p| bundle.build(p, *languages) }
 
   if docs
     SC.logger.debug('~ Building docs')</diff>
      <filename>bin/sc-build</filename>
    </modified>
    <modified>
      <diff>@@ -19,11 +19,12 @@ module SproutCore
       include SproutCore::Helpers::DomIdHelper
       include SproutCore::ViewHelpers
 
-      attr_reader :entry, :bundle, :entries, :filename, :language, :library, :renderer
+      attr_reader :entry, :bundle, :entries, :filename, :language, :library, :renderer, :unit_test, :platform
 
       def initialize(entry, bundle, deep=true)
         @entry = nil
         @language = entry.language
+        @platform = entry.platform
         @bundle = bundle
         @library = bundle.library
 
@@ -94,12 +95,31 @@ module SproutCore
 
         def _render(file_path)
           SC.logger.debug(&quot;~ Rendering #{file_path}&quot;)
-          input = File.read(file_path)
+          
+          # if this is a JS file, read the JS into a unit_test variable then
+          # output a special unit_test.rhtml file.
+          if file_path =~ /\.js$/
+            @unit_test = File.read(file_path)
+            
+            unit_test_template = File.join(File.dirname(__FILE__), 'test_template.rhtml')
+            input = File.read(unit_test_template)
+            
+          # Otherwise, just read in the source file
+          else
+            input = File.read(file_path)
+          end
+          
           @renderer = case file_path
-          when /\.rhtml$/, /\.html.erb$/
+            
+          # rhtml &amp; .html.erb get processed through Erubis. 
+          # JS files passed in for unit tests are also erubis.
+          when /\.js$/, /\.rhtml$/, /\.html.erb$/
             Sproutcore::Renderers::Erubis.new(self)
+            
+          # haml is processed through HAML
           when /\.haml$/
             Sproutcore::Renderers::Haml.new(self)
+          
           end
           _render_compiled_template( @renderer.compile(input) )
         end
@@ -132,9 +152,12 @@ module SproutCore
 
     end
 
-    # Building a test is just like building a single page except that we do not include
-    # all the other html templates in the project
-    def self.build_test(entry, bundle); build_html(entry, bundle, false); end
+    # Building a test is almost like building a single page except that we
+    # do not include all the other html templates.  Also, if the test is a
+    # JS file instead, we need to generate the HTML template.
+    def self.build_test(entry, bundle)
+      build_html(entry, bundle, false)
+    end
 
   end
 end</diff>
      <filename>lib/sproutcore/build_tools/html_builder.rb</filename>
    </modified>
    <modified>
      <diff>@@ -10,20 +10,23 @@ module SproutCore
     # The ResourceBuilder knows how
     class ResourceBuilder
 
-      attr_reader :bundle, :language, :filenames
+      attr_reader :bundle, :language, :platform, :filenames
 
       # Utility method you can call to get the items sorted by load order
-      def self.sort_entries_by_load_order(entries, language, bundle)
+      def self.sort_entries_by_load_order(entries, language, bundle, platform)
         filenames = entries.map { |e| e.filename }
         hashed = {}
         entries.each { |e| hashed[e.filename] = e }
 
-        sorted = self.new(filenames, language, bundle).required
+        sorted = self.new(filenames, language, bundle, platform).required
         sorted.map { |filename| hashed[filename] }
       end
 
-      def initialize(filenames, language, bundle)
-        @bundle = bundle; @language = language; @filenames = filenames
+      def initialize(filenames, language, bundle, platform)
+        @bundle = bundle
+        @language = language
+        @platform = platform
+        @filenames = filenames
       end
 
       # Simply returns the filenames in the order that they were required
@@ -81,7 +84,7 @@ module SproutCore
         return [lines, required] if required.include?(filename)
         required &lt;&lt; filename
 
-        entry = bundle.entry_for(filename, :hidden =&gt; :include, :language =&gt; language)
+        entry = bundle.entry_for(filename, :hidden =&gt; :include, :language =&gt; language, :platform =&gt; platform)
         if entry.nil?
           puts &quot;WARNING: Could not find require file: #{filename}&quot;
           return [lines, required]
@@ -197,7 +200,7 @@ module SproutCore
 
     def self.build_stylesheet(entry, bundle)
       filenames = entry.composite? ? entry.composite_filenames : [entry.filename]
-      builder = ResourceBuilder.new(filenames, entry.language, bundle)
+      builder = ResourceBuilder.new(filenames, entry.language, bundle, entry.platform)
       if output = builder.build
         FileUtils.mkdir_p(File.dirname(entry.build_path))
         f = File.open(entry.build_path, 'w')
@@ -208,7 +211,7 @@ module SproutCore
 
     def self.build_javascript(entry, bundle)
       filenames = entry.composite? ? entry.composite_filenames : [entry.filename]
-      builder = JavaScriptResourceBuilder.new(filenames, entry.language, bundle)
+      builder = JavaScriptResourceBuilder.new(filenames, entry.language, bundle, entry.platform)
       if output = builder.build
         FileUtils.mkdir_p(File.dirname(entry.build_path))
         f = File.open(entry.build_path, 'w')</diff>
      <filename>lib/sproutcore/build_tools/resource_builder.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,4 +1,5 @@
 require 'sproutcore/build_tools'
+require 'digest/md5'
 
 module SproutCore
 
@@ -74,6 +75,10 @@ module SproutCore
   #   MD5 digests instead of timestamps.  This will ensure uniqueness when
   #   building on multiple machines.
   #
+  # build_number:: The build number.  Defaults to timestamp
+  #
+  # prefered_platform:: preferred default platform.  Default :desktop
+  #
   class Bundle
 
     LONG_LANGUAGE_MAP = { :english =&gt; :en, :french =&gt; :fr, :german =&gt; :de, :japanese =&gt; :ja, :spanish =&gt; :es, :italian =&gt; :it }
@@ -94,6 +99,8 @@ module SproutCore
     attr_reader :build_mode, :layout
     attr_reader :make_resources_relative
     attr_reader :use_digest_tokens
+    
+    attr_reader :preferred_platform
 
     def library_root
       @library_root ||= library.nil? ? nil : library.root_path
@@ -132,6 +139,16 @@ module SproutCore
     end
     
     # ==== Returns
+    # The build number for the bundle.  If you supply a build number, that 
+    # will be used.  If not, then the build number will always be 
+    # 'development' in development mode and a number computed by MD5 hash
+    # of the entire bundle contents in production mode.
+    def build_number
+      return 'development' if build_mode == :development
+      return @build_number ||= SC.library.compute_build_number(self)
+    end
+    
+    # ==== Returns
     # The computed path to the layout rhtml.
     def layout_path
       return @layout_path unless @layout_path.nil?
@@ -236,6 +253,9 @@ module SproutCore
       
       @use_digest_tokens = opts[:use_digest_tokens] || (@build_mode == :production)
       
+      @preferred_platform = opts[:preferred_platform] || :desktop
+      @build_number = opts[:build_number]
+      
       reload!
     end
 
@@ -247,16 +267,18 @@ module SproutCore
     # The array of stylesheet entries sorted in load order.
     def sorted_stylesheet_entries(opts = {})
       opts[:language] ||= preferred_language
+      opts[:platform] ||= preferred_platform
       entries = entries_for(:stylesheet, opts)
-      BuildTools::ResourceBuilder.sort_entries_by_load_order(entries, opts[:language], self)
+      BuildTools::ResourceBuilder.sort_entries_by_load_order(entries, opts[:language], self, opts[:platform])
     end
 
     # ==== Returns
     # The array of javascript entries sorted in load order.
     def sorted_javascript_entries(opts = {})
       opts[:language] ||= preferred_language
+      opts[:platform] ||= preferred_platform
       entries = entries_for(:javascript, opts)
-      BuildTools::JavaScriptResourceBuilder.sort_entries_by_load_order(entries, opts[:language], self)
+      BuildTools::JavaScriptResourceBuilder.sort_entries_by_load_order(entries, opts[:language], self, opts[:platform])
     end
 
     # This method returns the manifest entries for resources of the specified
@@ -268,14 +290,16 @@ module SproutCore
     #
     # ==== Options
     # language::  The language to use.  Defaults to preferred language.
+    # platform::  The platform to use.  Defaults to preferred platform.
     # hidden::    Can be :none|:include|:only
     #
     def entries_for(resource_type, opts={})
       with_hidden = opts[:hidden] || :none
 
       language = opts[:language] || preferred_language
+      platform = opts[:platform] || preferred_platform
       mode = (opts[:build_mode] || build_mode).to_sym
-      manifest = manifest_for(language, mode)
+      manifest = manifest_for(language, mode, platform)
 
       ret = manifest.entries_for(resource_type)
 
@@ -295,14 +319,16 @@ module SproutCore
     #
     # ==== Options
     # language::  The language to use.  Defaults to preferred language
+    # platform::  The platform to use.  Defaults to preferred platform.
     # hidden::    Can be :none|:include|:only
     #
     def entry_for(resource_name, opts={})
       with_hidden = opts[:hidden] || :none
 
       language = opts[:language] || preferred_language
+      platform = opts[:platform] || preferred_platform
       mode = (opts[:build_mode] || build_mode).to_sym
-      manifest = manifest_for(language, mode)
+      manifest = manifest_for(language, mode, platform)
 
       ret = manifest.entry_for(resource_name)
 
@@ -316,9 +342,9 @@ module SproutCore
     end
 
     # Returns the entry for the specified URL.  This will extract the language
-    # from the URL and try to get the entry from both the manifest in the
-    # current build mode and in the production build mode (if one is
-    # provided)
+    # and platform from the URL and try to get the entry from both the 
+    # manifest in the current build mode and in the production build mode (if 
+    # one is provided)
     #
     # ==== Params
     # url&lt;String&gt;:: The url
@@ -327,15 +353,21 @@ module SproutCore
     # hidden::     Use :include,:none,:only to control hidden options
     # language::   Explicitly include the language.  Leave this out to
     #   autodetect from URL.
+    # platform::   Explicitly include the platform.  Leave this out to 
+    #   autodetect from URL.
     #
     def entry_for_url(url, opts={})
+
       # get the language
       opts[:language] ||= url.match(/^#{url_root}\/([^\/]+)\//).to_a[1] || url.match(/^#{index_root}\/([^\/]+)\//).to_a[1] || preferred_language
 
+      opts[:platform] ||= url.match(/^#{url_root}\/([^\/]+)\/([^\/]+)\//).to_a[2] || url.match(/^#{index_root}\/([^\/]+)\/([^\/]+)\//).to_a[2] || preferred_platform
+      
       # use the current build mode
       opts[:build_mode] = build_mode
       entries(opts).each do |entry|
         return entry if entry.url == url
+        return entry if entry.current_url == url
       end
 
       # try production is necessary...
@@ -343,6 +375,7 @@ module SproutCore
         opts[:build_mode] = :production
         entries(opts).each do |entry|
           return entry if entry.url == url
+          return entry if entry.current_url == url
         end
       end
 
@@ -351,20 +384,24 @@ module SproutCore
 
     # Helper method.  This will normalize a URL into one that can map directly
     # to an entry in the bundle.  If the URL is of a format that cannot be
-    # converted, returns nil.
+    # converted, returns the url.  In particular, this will look for all the 
+    # different ways you can request an index.html file and convert it to a
+    # canonical form
     #
     # ==== Params
     # url&lt;String&gt;:: The URL
     #
     def normalize_url(url)
 
-      # Get the default index.
-      if (url == index_root)
-        url = [index_root, preferred_language.to_s, 'index.html'].join('/')
-
-      # Requests to index_root/lang should have index.html appended to them
-      elsif /^#{index_root}\/[^\/\.]+$/ =~ url
-        url &lt;&lt; '/index.html'
+      # Parse the URL
+      matched = url.match(/^#{index_root}(\/([^\/\.]+))?(\/([^\/\.]+))?(\/([^\/\.]+))?(\/|(\/index\.html))?$/)
+      unless matched.nil?
+        matched_language = matched[2] || preferred_language
+        matched_platform = matched[4] || preferred_platform
+        matched_build_number = matched[6] || 'current'
+        url = [index_root, 
+          matched_language, matched_platform, matched_build_number,
+          'index.html'] * '/'
       end
 
       return url
@@ -374,14 +411,16 @@ module SproutCore
     #
     # ==== Options
     # language::  The language to use.  Defaults to preferred language
+    # platform::  The platform to use.  Defaults to preferred platform
     # hidden::    Can be :none|:include|:only
     #
     def entries(opts ={})
       with_hidden = opts[:hidden] || :none
 
       language = opts[:language] || preferred_language
+      platform = opts[:platform] || preferred_platform
       mode = (opts[:build_mode] || build_mode).to_sym
-      manifest = manifest_for(language, mode)
+      manifest = manifest_for(language, mode, platform)
 
       ret = manifest.entries
 
@@ -500,17 +539,17 @@ module SproutCore
       @seen &lt;&lt; entry unless @seen.nil?
     end
 
-    # This will perform a complete build for the named language
-    def build_language(language)
-      SC.logger.info(&quot;~ Language: #{language}&quot;)
-      build_entries(entries(:language =&gt; language))
+    # This will perform a build for a single language
+    def build_language(language, platform=nil)
+      SC.logger.info(&quot;~ Language/Platform: #{[language,platform].compact.join('/') }&quot;)
+      build_entries(entries(:language =&gt; language, :platform =&gt; platform))
       SC.logger.debug(&quot;~ Done.\n&quot;)
     end
 
     # This will perform a complete build for all languages that have a
     # matching lproj. You can also pass in an array of languages you would
     # like to build
-    def build(*languages)
+    def build(platform, *languages)
 
       # Get the installed languages (and the preferred language, just in case)
       languages = languages.flatten
@@ -521,7 +560,7 @@ module SproutCore
       SC.logger.debug(&quot;~ Source Root: #{source_root}&quot;)
       SC.logger.debug(&quot;~ Build Root:  #{build_root}&quot;)
 
-      languages.uniq.each { |lang| build_language(lang) }
+      languages.uniq.each { |lang| build_language(lang, platform) }
 
       # After build is complete, try to copy the index.html file of the
       # preferred language to the build_root
@@ -679,18 +718,28 @@ module SproutCore
 
     # Returns the bundle manifest for the specified language and build mode.
     # The manifest will be created if it does not yet exist.
-    def manifest_for(language, build_mode)
-      manifest_key = [build_mode.to_s, language.to_s].join(':').to_sym
-      @manifests[manifest_key] ||= BundleManifest.new(self, language.to_sym, build_mode.to_sym)
+    def manifest_for(language, build_mode, platform)
+      manifest_key = [build_mode.to_s, language.to_s, platform.to_s].join(':').to_sym
+      @manifests[manifest_key] ||= BundleManifest.new(self, language.to_sym, build_mode.to_sym, platform.to_sym)
     end
 
     # ==== Returns
+    # Platforms installed in the source directory
+    #
+    def installed_platforms
+      ret = Dir.glob(File.join(source_root, '*.platform')).map do |x|
+        x.match(/([^\/]+)\.platform$/).to_a[1]
+      end
+      ret &lt;&lt; preferred_platform
+      ret.compact.map { |x| x.to_sym }.uniq
+    end
+    
+    # ==== Returns
     # Languages installed in the source directory
     #
     def installed_languages
-      ret = Dir.glob(File.join(source_root,'*.lproj')).map do |x|
-        x.match(/([^\/]+)\.lproj$/).to_a[1]
-      end
+      ret = Dir.glob(File.join(source_root,'*.lproj')) + Dir.glob(File.join(source_root, '*.platform', '*.lproj'))
+      ret.map! { |x| x.match(/([^\/]+)\.lproj$/).to_a[1] }
       ret &lt;&lt; preferred_language
       ret.compact.map { |x| LONG_LANGUAGE_MAP[x.to_sym] || x.to_sym }.uniq
     end</diff>
      <filename>lib/sproutcore/bundle.rb</filename>
    </modified>
    <modified>
      <diff>@@ -6,21 +6,30 @@ module SproutCore
   # A Bundle Manifest describes all of the resources in a bundle, including
   # mapping their source paths, destination paths, and urls.
   #
-  # A Bundle will create a manifest for every language you request from it.
-  # If you invoke reload! on the bundle, it will dispose of its manifests and
-  # rebuild them.
+  # A Bundle will create a manifest for every language and platform you 
+  # request from it. If you invoke reload! on the bundle, it will dispose of 
+  # its manifests and rebuild them.
   #
   class BundleManifest
 
-    CACHED_TYPES = [:javascript, :stylesheet, :fixture, :test]
+    CACHED_TYPES    = [:javascript, :stylesheet, :fixture, :test]
     SYMLINKED_TYPES = [:resource]
+    
+    PLATFORM_MATCH  = /^([^\/]+)\.platform\//
+    LPROJ_MATCH     = /^([^\/]+\.platform\/)?([^\/]+\.lproj)\//
+
+    NORMALIZED_TYPE_EXTENSIONS = {
+      :stylesheet =&gt; { :sass =&gt; :css },
+      :test =&gt; { '.+' =&gt; :html }
+    }
 
-    attr_reader :bundle, :language, :build_mode
+    attr_reader :bundle, :language, :build_mode, :platform
 
-    def initialize(bundle, language, build_mode)
+    def initialize(bundle, language, build_mode, platform)
       @bundle = bundle
       @language = language
       @build_mode = build_mode
+      @platform = platform
       @entries_by_type = {} # entries by type
       @entries_by_filename = {} # entries by files
       build!
@@ -168,7 +177,8 @@ module SproutCore
     end
 
     # Build a catalog of entries for this manifest.  This will simply filter 
-    # out the files that don't actually belong in the current language
+    # out the files that don't actually belong in the current language or 
+    # platform
     def catalog_entries
 
       # Entries arranged by resource filename
@@ -180,8 +190,9 @@ module SproutCore
       default_lproj = bundle.lproj_for(bundle.preferred_language)
       target_lproj = bundle.lproj_for(language)
 
-      # Any files living in the two lproj dirs will be shunted off into these arrays
-      # and processed later to make sure we process them in the right order
+      # Any files living in the two lproj dirs will be shunted off into these 
+      # arrays and processed later to make sure we process them in the right 
+      # order
       default_lproj_files = []
       target_lproj_files = []
 
@@ -192,33 +203,46 @@ module SproutCore
         # Get source type.  Skip any without a useful type
         next if (src_type = type_of(src_path)) == :skip
 
+        # Get target platform (if there is one).  Skip is not target platform
+        if current_platform = src_path.match(PLATFORM_MATCH).to_a[1]
+          next if current_platform.to_sym != platform
+        end
+        
         # Get current lproj (if there is one).  Skip if not default or current
-        if current_lproj = src_path.match(/^([^\/]+\.lproj)\//).to_a[1]
+        if current_lproj = src_path.match(LPROJ_MATCH).to_a[2]
           next if (current_lproj != default_lproj) &amp;&amp; (current_lproj != target_lproj)
         end
 
-        # OK, pass all of our validations.  Go ahead and build an entry for this
-        # Add entry to list of entries for appropriate lproj if localized
+        # OK, pass all of our validations.  Go ahead and build an entry for 
+        # this. Add entry to list of entries for appropriate lproj if 
+        # localized.
+        #
+        # Note that entries are namespaced by platform.  This way non-platform
+        # specific entries will not be overwritten by platfor-specific entries
+        # of the same name.
+        #
         entry = build_entry_for(src_path, src_type)
+        entry_key = [current_platform||'', entry.filename].join(':')
         case current_lproj
         when default_lproj
-          default_lproj_entries[entry.filename] = entry
+          default_lproj_entries[entry_key] = entry
         when target_lproj
-          target_lproj_entries[entry.filename] = entry
+          target_lproj_entries[entry_key] = entry
         else
 
           # Be sure to mark any
-          entries[entry.filename] = entry
+          entries[entry_key] = entry
         end
       end
       Dir.chdir(old_wd) # restore wd
 
-      # Now, new in default and target lproj entries.  This will overwrite entries that exist
-      # in both places.
+      # Now, merge in default and target lproj entries.  This will overwrite 
+      # entries that exist in both places.
       entries.merge!(default_lproj_entries)
       entries.merge!(target_lproj_entries)
 
-      # Finally, entries will need to be grouped by type to allow further processing.
+      # Finally, entries will need to be grouped by type to allow further 
+      # processing.
       ret = {}
       entries.values.each { |entry| (ret[entry.type] ||= []) &lt;&lt; entry }
       return ret
@@ -275,6 +299,16 @@ module SproutCore
     # source_root) This should assume we are in going to simply build each
     # resource into the build root without combining files, but not using our
     # _src symlink magic.
+    #
+    # +Params+
+    #
+    # src_path:: the source path, relative to the bunlde.source_root
+    # src_type:: the detected source type (from type_of())
+    # composite:: Array of entries that should be combined to form this or nil
+    # hide_composite:: Makes composit entries hidden if !composite.nil?
+    #
+    # +Returns: Entry
+    #
     def build_entry_for(src_path, src_type, composite=nil, hide_composite = true)
       ret = ManifestEntry.new
       ret.ext = File.extname(src_path)[1..-1] || '' # easy stuff
@@ -282,17 +316,20 @@ module SproutCore
       ret.original_path = src_path
       ret.hidden = false
       ret.language = language
+      ret.platform = platform
       ret.use_digest_tokens = bundle.use_digest_tokens
 
-      # the filename is the src_path less any lproj in the front
-      ret.filename = src_path.gsub(/^[^\/]+.lproj\//,'')
+      # the filename is the src_path less any lproj or platform in the front
+      ret.filename = src_path.gsub(LPROJ_MATCH,'')
 
       # the source path is just the combine source root + the path
-      ret.source_path = (composite.nil?) ? File.join(bundle.source_root, src_path) : nil
+      # Composite entries do not have a source path b/c they are generated
+      # dynamically.
+      ret.source_path = composite.nil? ? File.join(bundle.source_root, src_path) : nil
 
       # set the composite property.  The passed in array should contain other
       # entries if hide_composite is true, then hide the composite items as
-      # well
+      # well.  
       unless composite.nil?
         composite.each { |x| x.hidden = true } if hide_composite
         
@@ -303,45 +340,75 @@ module SproutCore
         ret.composite = composite.dup 
       end
 
-      # The build path is the build_root + the filename
-      # The URL is the url root + the language code + filename
-      # also add in _cache or _sym in certain cases.  This is just more
-      # efficient than doing it later.
+      # PREPARE BUILD_PATH and URL
+      
+      # The main index.html file is served from the index_Root.  All other
+      # resourced are served from the URL root.
       url_root = (src_path == 'index.html') ? bundle.index_root : bundle.url_root
-      cache_link = nil
-      use_source_directly =false
 
-      # Note: you can only access real resources via the cache.  If the entry
-      # is a composite then do not go through cache.
+      # Setup special cases.  Certain types of files are processed and then
+      # cached in development mode (i.e. JS + CSS).  Other resources are
+      # simply served up directly without any processing or building.  See
+      # constants for types.
+      cache_link = nil; use_source_directly =false
       if (self.build_mode == :development) #&amp;&amp; composite.nil?
         cache_link = '_cache' if CACHED_TYPES.include?(src_type)
         use_source_directly = true if SYMLINKED_TYPES.include?(src_type)
       end
 
+      # If this resource should be served directly, setup both the build_path
+      # and URL to point to a special URL that maps directly to the resource.
+      # This is only useful in development mode
       ret.use_source_directly = use_source_directly
       if use_source_directly
         ret.build_path = File.join(bundle.build_root, '_src', src_path)
         ret.url = [url_root, '_src', src_path].join('/')
+        
+      # If the resource is not served directly, then calculate the actual 
+      # build path and URL for production mode.  The build path is the 
+      # build root + language + platform + (cache_link || build_number) +
+      # filename 
+      #
+      # The URL is the url_root + current_language + current_platform + (cache_link)
       else
-        ret.build_path = File.join(*[bundle.build_root, language.to_s, cache_link, ret.filename].compact)
-        ret.url = [url_root, language.to_s, cache_link, ret.filename].compact.join('/')
+        path_parts = [bundle.build_root, language.to_s, platform.to_s, 
+           (cache_link || bundle.build_number.to_s), ret.filename]
+        ret.build_path = File.join(*path_parts.compact)
+        
+        path_parts[0] = url_root
+        ret.url = path_parts.compact.join('/')
+        
+        path_parts[3] = 'current' # create path to &quot;current&quot; build
+        ret.current_url = path_parts.compact.join('/')
+        
+      end
+      
+      # Convert the input source type an output type.
+      if sub_type = NORMALIZED_TYPE_EXTENSIONS[ret.type]
+        sub_type.each do | matcher, ext |
+          matcher = /\.#{matcher.to_s}$/; ext = &quot;.#{ext.to_s}&quot;
+          ret.build_path.sub!(matcher, ext)
+          ret.url.sub!(matcher, ext)
+        end
       end
-      ret.build_path.sub!(/\.sass$/, '.css')
-      ret.url.sub!(/.sass$/, '.css')
 
       # Done.
       return ret
     end
 
-    # Lookup the timestamp on the source path and interpolate that into the filename URL.
-    # also insert the _cache element.
+    # Lookup the timestamp on the source path and interpolate that into the 
+    # filename URL and build path.  This should only be called on entries
+    # that are to be cached (in development mode)
     def setup_timestamp_token(entry)
       timestamp = bundle.use_digest_tokens ? entry.digest : entry.timestamp
+      
+      # add timestamp or digest to URL 
       extname = File.extname(entry.url)
-      entry.url = entry.url.gsub(/#{extname}$/,&quot;-#{timestamp}#{extname}&quot;) # add timestamp
+      entry.url.gsub!(/#{extname}$/,&quot;-#{timestamp}#{extname}&quot;) 
 
+      # add timestamp or digest to build path
       extname = File.extname(entry.build_path)
-      entry.build_path = entry.build_path.gsub(/#{extname}$/,&quot;-#{timestamp}#{extname}&quot;) # add timestamp
+      entry.build_path.gsub!(/#{extname}$/,&quot;-#{timestamp}#{extname}&quot;)
     end
   end
 
@@ -350,7 +417,8 @@ module SproutCore
   # filename::     path relative to the built language (e.g. sproutcore/en) less file extension
   # ext::          the file extension
   # source_path:: absolute paths into source that will comprise this resource
-  # url::          the url that should be used to reference this resource in the current mode.
+  # url::          the url that should be used to reference this resource in the current build mode.
+  # current_url::  the url that can be used to reference this resource, substituting &quot;current&quot; for a build number
   # build_path::   absolute path to the compiled resource
   # type::         the top-level category
   # original_path:: save the original path used to build this entry
@@ -359,8 +427,9 @@ module SproutCore
   # language::     the language in use when this entry was created
   # composite::    If set, this will contain the filenames of other resources that should be combined to form this resource.
   # bundle:: the owner bundle for this entry
+  # platform:: the target platform for the entry, if any
   #
-  class ManifestEntry &lt; Struct.new(:filename, :ext, :source_path, :url, :build_path, :type, :original_path, :hidden, :use_source_directly, :language, :use_digest_tokens)
+  class ManifestEntry &lt; Struct.new(:filename, :ext, :source_path, :url, :build_path, :type, :original_path, :hidden, :use_source_directly, :language, :use_digest_tokens, :platform, :current_url)
     def to_hash
       ret = {}
       self.members.zip(self.values).each { |p| ret[p[0]] = p[1] }
@@ -431,8 +500,9 @@ module SproutCore
 
     # Returns a URL that takes into account caching requirements.
     def cacheable_url
-      token = (use_digest_tokens) ? digest : timestamp
-      [url, token].compact.join('?')
+      url
+      #token = (use_digest_tokens) ? digest : timestamp
+      #[url, token].compact.join('?')
     end
 
     # :stopdoc:</diff>
      <filename>lib/sproutcore/bundle_manifest.rb</filename>
    </modified>
    <modified>
      <diff>@@ -18,6 +18,7 @@ module SproutCore
       def stylesheets_for_client(bundle_name = nil, opts = {})
 
         opts[:language] ||= language
+        opts[:platform] ||= platform
         
         # Set the import method to use the standard &lt;link&gt; tag, if not set
         include_method = opts[:include_method] ||= :link
@@ -64,6 +65,7 @@ module SproutCore
       def javascripts_for_client(bundle_name = nil, opts = {})
 
         opts[:language] ||= language
+        opts[:platform] ||= platform
 
         # Get bundle
         cur_bundle = bundle_name.nil? ? bundle : library.bundle_for(bundle_name)
@@ -94,6 +96,7 @@ module SproutCore
       # Returns the URL for the named resource
       def static_url(resource_name, opts = {})
         opts[:language] ||= language
+        opts[:platform] ||= platform
         entry = bundle.find_resource_entry(resource_name, opts)
         entry.nil? ? '' : entry.cacheable_url
       end
@@ -102,6 +105,7 @@ module SproutCore
       def loc(string, opts = {})
         string = string.nil? ? '' : string.to_s
         opts[:language] ||= language
+        opts[:platform] ||= platform
         bundle.strings_hash(opts)[string] || string
       end
 </diff>
      <filename>lib/sproutcore/helpers/static_helper.rb</filename>
    </modified>
    <modified>
      <diff>@@ -348,6 +348,21 @@ module SproutCore
       self.bundle_installer.remove(bundle_name, opts)
     end
     
+    # Computes a build number for the bundle by generating a SHA1 hash.  The
+    # return value is cached to speed up future requests.
+    def compute_build_number(bundle)
+      @cached_build_numbers ||= {}
+      src = bundle.source_root
+      ret = @cached_build_numbers[src]
+      return ret unless ret.nil?
+
+      digests = Dir.glob(File.join(src, '**', '*.*')).map do |path|
+        (File.exists?(path) &amp;&amp; !File.directory?(path)) ? Digest::SHA1.hexdigest(File.read(path)) : '0000'
+      end
+      ret = @cached_build_numbers[src] = Digest::SHA1.hexdigest(digests.join)
+      return ret 
+    end
+      
     protected
 
     # Load the library at the specified path.  Loads the sc-config.rb if it
@@ -379,6 +394,14 @@ module SproutCore
       end
 
       # Override any all options with load_opts
+      if build_numbers = @load_opts[:build_numbers]
+        @load_opts.delete(:build_numbers)
+        build_numbers.each do | bundle_name, build_number |
+          env = @environment[bundle_name.to_sym] ||= {}
+          env[:build_number] = build_number
+        end
+      end
+
       (@environment[:all] ||= {}).merge!(@load_opts)
 
     end</diff>
      <filename>lib/sproutcore/library.rb</filename>
    </modified>
    <modified>
      <diff>@@ -69,8 +69,11 @@ module SproutCore
 
         # Check for a few special urls for built-in services and route them off
         case url
+          
+        # Returns the JSON file that contains the current list of tests.
         when &quot;#{current_bundle.url_root}/-tests/index.js&quot;
           ret = handle_test(url)
+          
         when &quot;#{current_bundle.index_root}/-docs/index.html&quot;
           ret = (request.method == :post) ? handle_doc(url) : handle_resource(url)
 </diff>
      <filename>lib/sproutcore/merb/bundle_controller.rb</filename>
    </modified>
    <modified>
      <diff>@@ -62,7 +62,7 @@ module SproutCore
         %{#{key} = #{opts[:class] || 'SC.View'}.extend({\n  #{ opts[:properties] }\n});}
       end
       ret &lt;&lt; %{#{prefix}.page = SC.Page.create({\n#{ outlets * &quot;,\n\n&quot; }\n}); }
-      bundle ? SproutCore::BuildTools::JavaScriptResourceBuilder.new(nil, nil, bundle).join(ret) : ret*&quot;\n&quot;
+      bundle ? SproutCore::BuildTools::JavaScriptResourceBuilder.new(nil, nil, bundle, nil).join(ret) : ret*&quot;\n&quot;
     end
 
     def self.render_css</diff>
      <filename>lib/sproutcore/view_helpers.rb</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>757b6396a1f228de7c3e9b83bf492a15a652afce</id>
    </parent>
  </parents>
  <author>
    <name>Charles Jolley</name>
    <email>charles@sproutit.com</email>
  </author>
  <url>http://github.com/sproutit/sproutcore-buildtools/commit/ad2154a56ac48688f286e04f23c4011144636aa7</url>
  <id>ad2154a56ac48688f286e04f23c4011144636aa7</id>
  <committed-date>2008-11-11T21:19:56-08:00</committed-date>
  <authored-date>2008-11-11T21:19:56-08:00</authored-date>
  <message>* Added support for using build numbers in URL strings instead of query string parameters.  This will make caches flush more reliably.

* Added support for building multiple platforms.  This is to be used with mobile platforms.

* Unit tests can now be simple .js files.  These files will be inserted into an html template automatically.</message>
  <tree>e4397df06f4ac6fd7e1a8337253dc028c4247aa8</tree>
  <committer>
    <name>Charles Jolley</name>
    <email>charles@sproutit.com</email>
  </committer>
</commit>
