diff --git a/.gitignore b/.gitignore index a2082370b..3c4c506c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ .rspec results .*.sw[op] +*.wixobj +downloads/* +pkg/* +stagedir/* +wix/fragments/* diff --git a/README_DEVELOPER.md b/README_DEVELOPER.md index aa6fb2bf4..f38067a0e 100644 --- a/README_DEVELOPER.md +++ b/README_DEVELOPER.md @@ -1,91 +1,98 @@ # Developer README # This file is intended to provide a place for developers and contributors to document what other developers need to know about changes made to Puppet. # UTF-8 Handling # As Ruby 1.9 becomes more commonly used with Puppet, developers should be aware of major changes to the way Strings and Regexp objects are handled. Specifically, every instance of these two classes will have an encoding attribute determined in a number of ways. * If the source file has an encoding specified in the magic comment at the top, the instance will take on that encoding. * Otherwise, the encoding will be determined by the LC\_LANG or LANG environment variables. * Otherwise, the encoding will default to ASCII-8BIT ## References ## Excellent information about the differences between encodings in Ruby 1.8 and Ruby 1.9 is published in this blog series: [Understanding M17n](http://links.puppetlabs.com/understanding_m17n) ## Encodings of Regexp and String instances ## In general, please be aware that Ruby 1.9 regular expressions need to be compatible with the encoding of a string being used to match them. If they are not compatible you can expect to receive and error such as: Encoding::CompatibilityError: incompatible encoding regexp match (ASCII-8BIT regexp with UTF-8 string) In addition, some escape sequences were valid in Ruby 1.8 are no longer valid in 1.9 if the regular expression is not marked as an ASCII-8BIT object. You may expect errors like this in this situation: SyntaxError: (irb):7: invalid multibyte escape: /\xFF/ This error is particularly common when serializing a string to other representations like JSON or YAML. To resolve the problem you can explicitly mark the regular expression as ASCII-8BIT using the /n flag: "a" =~ /\342\230\203/n Finally, any time you're thinking of a string as an array of bytes rather than an array of characters, common when escaping a string, you should work with everything in ASCII-8BIT. Changing the encoding will not change the data itself and allow the Regexp and the String to deal with bytes rather than characters. Puppet provides a monkey patch to String which returns an encoding suitable for byte manipulations: # Example of how to escape non ASCII printable characters for YAML. >> snowman = "☃" >> snowman.to_ascii8bit.gsub(/([\x80-\xFF])/n) { |x| "\\x#{x.unpack("C")[0].to_s(16)} } => "\\xe2\\x98\\x83" If the Regexp is not marked as ASCII-8BIT using /n, then you can expect the SyntaxError, invalid multibyte escape as mentioned above. # Windows # If you'd like to run Puppet from source on Windows platforms, the include `ext/envpuppet.bat` will help. All file paths in the Puppet code base should use a path separator of / regardless of Windows or Unix filesystem. To quickly run Puppet from source, assuming you already have Ruby installed from [rubyinstaller.org](http://rubyinstaller.org). gem install sys-admin win32-process win32-dir win32-taskscheduler --no-rdoc --no-ri gem install win32-service --platform=mswin32 --no-rdoc --no-ri --version 0.7.1 net use Z: "\\vmware-host\Shared Folders" /persistent:yes Z: cd set PATH=%PATH%;Z:\\ext envpuppet puppet --version 2.7.9 Some spec tests are known to fail on Windows, e.g. no mount provider on Windows, so use the following rspec exclude filter: cd envpuppet rspec --tag ~fails_on_windows spec This will give you a shared filesystem with your Mac and allow you to run Puppet directly from source without using install.rb or copying files around. +## Building Windows Packages ## + +Please see the README files in `tasks/windows/` for more information about +building MSI packages of Puppet for Windows. Please see also +[#11205](http://projects.puppetlabs.com/issues/11205) for up to date progress +on this project. + EOF diff --git a/tasks/rake/contrib/progressbar.rb b/tasks/rake/contrib/progressbar.rb new file mode 100644 index 000000000..f8d219dc6 --- /dev/null +++ b/tasks/rake/contrib/progressbar.rb @@ -0,0 +1,237 @@ +# = progressbar.rb +# +# == Copyright (C) 2001 Satoru Takabayashi +# +# Ruby License +# +# This module is free software. You may use, modify, and/or redistribute this +# software under the same terms as Ruby. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. +# +# == Author(s) +# +# * Satoru Takabayashi + +# Author:: Satoru Takabayashi +# Copyright:: Copyright (c) 2001 Satoru Takabayashi +# License:: Ruby License + +# = Console Progress Bar +# +# Console::ProgressBar is a terminal-based progress bar library. +# +# == Usage +# +# pbar = ConsoleProgressBar.new( "Demo", 100 ) +# 100.times { pbar.inc } +# pbar.finish +# + +module Console; end + +class Console::ProgressBar + + def initialize(title, total, out = STDERR) + @title = title + @total = total + @out = out + @bar_length = 80 + @bar_mark = "o" + @total_overflow = true + @current = 0 + @previous = 0 + @is_finished = false + @start_time = Time.now + @format = "%-14s %3d%% %s %s" + @format_arguments = [:title, :percentage, :bar, :stat] + show_progress + end + + private + def convert_bytes (bytes) + if bytes < 1024 + sprintf("%6dB", bytes) + elsif bytes < 1024 * 1000 # 1000kb + sprintf("%5.1fKB", bytes.to_f / 1024) + elsif bytes < 1024 * 1024 * 1000 # 1000mb + sprintf("%5.1fMB", bytes.to_f / 1024 / 1024) + else + sprintf("%5.1fGB", bytes.to_f / 1024 / 1024 / 1024) + end + end + + def transfer_rate + bytes_per_second = @current.to_f / (Time.now - @start_time) + sprintf("%s/s", convert_bytes(bytes_per_second)) + end + + def bytes + convert_bytes(@current) + end + + def format_time (t) + t = t.to_i + sec = t % 60 + min = (t / 60) % 60 + hour = t / 3600 + sprintf("%02d:%02d:%02d", hour, min, sec); + end + + # ETA stands for Estimated Time of Arrival. + def eta + if @current == 0 + "ETA: --:--:--" + else + elapsed = Time.now - @start_time + eta = elapsed * @total / @current - elapsed; + sprintf("ETA: %s", format_time(eta)) + end + end + + def elapsed + elapsed = Time.now - @start_time + sprintf("Time: %s", format_time(elapsed)) + end + + def stat + if @is_finished then elapsed else eta end + end + + def stat_for_file_transfer + if @is_finished then + sprintf("%s %s %s", bytes, transfer_rate, elapsed) + else + sprintf("%s %s %s", bytes, transfer_rate, eta) + end + end + + def eol + if @is_finished then "\n" else "\r" end + end + + def bar + len = percentage * @bar_length / 100 + sprintf("|%s%s|", @bar_mark * len, " " * (@bar_length - len)) + end + + def percentage + if @total.zero? + 100 + else + @current * 100 / @total + end + end + + def title + @title[0,13] + ":" + end + + def get_width + # FIXME: I don't know how portable it is. + default_width = 80 + begin + tiocgwinsz = 0x5413 + data = [0, 0, 0, 0].pack("SSSS") + if @out.ioctl(tiocgwinsz, data) >= 0 then + rows, cols, xpixels, ypixels = data.unpack("SSSS") + if cols >= 0 then cols else default_width end + else + default_width + end + rescue Exception + default_width + end + end + + def show + arguments = @format_arguments.map {|method| send(method) } + line = sprintf(@format, *arguments) + + width = get_width + if line.length == width - 1 + @out.print(line + eol) + elsif line.length >= width + @bar_length = [@bar_length - (line.length - width + 1), 0].max + if @bar_length == 0 then @out.print(line + eol) else show end + else #line.length < width - 1 + @bar_length += width - line.length + 1 + show + end + end + + def show_progress + if @total.zero? + cur_percentage = 100 + prev_percentage = 0 + else + cur_percentage = (@current * 100 / @total).to_i + prev_percentage = (@previous * 100 / @total).to_i + end + + if cur_percentage > prev_percentage || @is_finished + show + end + end + + public + def file_transfer_mode + @format_arguments = [:title, :percentage, :bar, :stat_for_file_transfer] + end + + def bar_mark= (mark) + @bar_mark = String(mark)[0..0] + end + + def total_overflow= (boolv) + @total_overflow = boolv ? true : false + end + + def format= (format) + @format = format + end + + def format_arguments= (arguments) + @format_arguments = arguments + end + + def finish + @current = @total + @is_finished = true + show_progress + end + + def halt + @is_finished = true + show_progress + end + + def set (count) + if count < 0 + raise "invalid count less than zero: #{count}" + elsif count > @total + if @total_overflow + @total = count + 1 + else + raise "invalid count greater than total: #{count}" + end + end + @current = count + show_progress + @previous = @current + end + + def inc (step = 1) + @current += step + @current = @total if @current > @total + show_progress + @previous = @current + end + + def inspect + "(ProgressBar: #{@current}/#{@total})" + end + +end diff --git a/tasks/rake/contrib/uri_ext.rb b/tasks/rake/contrib/uri_ext.rb new file mode 100644 index 000000000..41fe9038c --- /dev/null +++ b/tasks/rake/contrib/uri_ext.rb @@ -0,0 +1,314 @@ +# +# I've striped down dependencies on Net::SSH and Facets to +# stay as simple as possible. +# +# Original code from Assaf Arkin in the buildr project, released under Apache +# License [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) +# +# Licensed to Puppet Labs under one or more contributor license agreements. +# See the NOTICE file distributed with this work for additional information +# regarding copyright ownership. Puppet Labs licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of the License +# at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +require 'cgi' +require 'uri' +require 'net/http' +require 'net/https' +require 'tempfile' +require 'fileutils' + +# show progress of download +require File.join(File.dirname(__FILE__), 'progressbar') + +# Not quite open-uri, but similar. Provides read and write methods for the resource represented by the URI. +# Currently supports reads for URI::HTTP and writes for URI::SFTP. Also provides convenience methods for +# downloads and uploads. +module URI + # Raised when trying to read/download a resource that doesn't exist. + class NotFoundError < RuntimeError; end + + class << self + # :call-seq: + # read(uri, options?) => content + # read(uri, options?) { |chunk| ... } + # + # Reads from the resource behind this URI. The first form returns the content of the resource, + # the second form yields to the block with each chunk of content (usually more than one). + # + # For example: + # File.open "image.jpg", "w" do |file| + # URI.read("http://example.com/image.jpg") { |chunk| file.write chunk } + # end + # Shorter version: + # File.open("image.jpg", "w") { |file| file.write URI.read("http://example.com/image.jpg") } + # + # Supported options: + # * :modified -- Only download if file modified since this timestamp. Returns nil if not modified. + # * :progress -- Show the progress bar while reading. + def read(uri, options = nil, &block) + uri = URI.parse(uri.to_s) unless URI === uri + uri.read(options, &block) + end + + # :call-seq: + # download(uri, target, options?) + # + # Downloads the resource to the target. + # + # The target may be a file name (string or task), in which case the file is created from the resource. + # The target may also be any object that responds to +write+, e.g. File, StringIO, Pipe. + # + # Use the progress bar when running in verbose mode. + def download(uri, target, options = nil) + uri = URI.parse(uri.to_s) unless URI === uri + uri.download(target, options) + end + + # :call-seq: + # write(uri, content, options?) + # write(uri, options?) { |bytes| .. } + # + # Writes to the resource behind the URI. The first form writes the content from a string or an object + # that responds to +read+ and optionally +size+. The second form writes the content by yielding to the + # block. Each yield should return up to the specified number of bytes, the last yield returns nil. + # + # For example: + # File.open "killer-app.jar", "rb" do |file| + # write("sftp://localhost/jars/killer-app.jar") { |chunk| file.read(chunk) } + # end + # Or: + # write "sftp://localhost/jars/killer-app.jar", File.read("killer-app.jar") + # + # Supported options: + # * :progress -- Show the progress bar while reading. + def write(uri, *args, &block) + uri = URI.parse(uri.to_s) unless URI === uri + uri.write(*args, &block) + end + end + + class Generic + + # :call-seq: + # read(options?) => content + # read(options?) { |chunk| ... } + # + # Reads from the resource behind this URI. The first form returns the content of the resource, + # the second form yields to the block with each chunk of content (usually more than one). + # + # For options, see URI::read. + def read(options = nil, &block) + fail "This protocol doesn't support reading (yet, how about helping by implementing it?)" + end + + # :call-seq: + # download(target, options?) + # + # Downloads the resource to the target. + # + # The target may be a file name (string or task), in which case the file is created from the resource. + # The target may also be any object that responds to +write+, e.g. File, StringIO, Pipe. + # + # Use the progress bar when running in verbose mode. + def download(target, options = {}) + case target + when String + # If download breaks we end up with a partial file which is + # worse than not having a file at all, so download to temporary + # file and then move over. + modified = File.stat(target).mtime if File.exist?(target) + temp = nil + Tempfile.open(File.basename(target)) do |tf| + tf.binmode + read(options.merge(:modified => modified)) { |chunk| tf.write chunk } + temp = tf + end + FileUtils.mkpath(File.dirname(target)) + FileUtils.move(temp.path, target) + when File + read(options.merge(:modified => target.mtime)) { |chunk| target.write chunk } + target.flush + else + raise ArgumentError, "Expecting a target that is either a file name (string, task) or object that responds to write (file, pipe)." unless target.respond_to?(:write) + read(options) { |chunk| target.write chunk } + target.flush + end + end + + # :call-seq: + # write(content, options?) + # write(options?) { |bytes| .. } + # + # Writes to the resource behind the URI. The first form writes the content from a string or an object + # that responds to +read+ and optionally +size+. The second form writes the content by yielding to the + # block. Each yield should return up to the specified number of bytes, the last yield returns nil. + # + # For options, see URI::write. + def write(*args, &block) + options = args.pop if Hash === args.last + options ||= {} + if String === args.first + ios = StringIO.new(args.first, "r") + write(options.merge(:size => args.first.size)) { |bytes| ios.read(bytes) } + elsif args.first.respond_to?(:read) + size = args.first.size rescue nil + write({ :size => size }.merge(options)) { |bytes| args.first.read(bytes) } + elsif args.empty? && block + write_internal(options, &block) + else + raise ArgumentError, "Either give me the content, or pass me a block, otherwise what would I upload?" + end + end + + protected + + # :call-seq: + # with_progress_bar(enable, file_name, size) { |progress| ... } + # + # Displays a progress bar while executing the block. The first argument must be true for the + # progress bar to show (TTY output also required), as a convenient for selectively using the + # progress bar from a single block. + # + # The second argument provides a filename to display, the third its size in bytes. + # + # The block is yielded with a progress object that implements a single method. + # Call << for each block of bytes down/uploaded. + def with_progress_bar(enable, file_name, size) #:nodoc: + file_name = CGI.unescape(file_name) + if enable && $stdout.isatty + progress_bar = Console::ProgressBar.new(file_name, size) + # Extend the progress bar so we can display count/total. + class << progress_bar + def total() + convert_bytes(@total) + end + end + # Squeeze the filename into 30 characters. + unescaped = CGI.unescape(file_name) + if unescaped.size > 30 + base, ext = File.basename(unescaped), File.extname(unescaped) + truncated = "#{base[0..26-ext.to_s.size]}..#{ext}" + else + truncated = unescaped + end + progress_bar.format = "#{truncated}: %3d%% %s %s/%s %s" + progress_bar.format_arguments = [:percentage, :bar, :bytes, :total, :stat] + progress_bar.bar_mark = "o" + + begin + class << progress_bar + def <<(bytes) + inc bytes.respond_to?(:size) ? bytes.size : bytes + end + end + yield progress_bar + ensure + progress_bar.finish + end + else + progress_bar = Object.new + class << progress_bar + def <<(bytes) + end + end + yield progress_bar + end + end + + # :call-seq: + # proxy_uri() => URI? + # + # Returns the proxy server to use. Obtains the proxy from the relevant environment variable (e.g. HTTP_PROXY). + # Supports exclusions based on host name and port number from environment variable NO_PROXY. + def proxy_uri() + proxy = ENV["#{scheme.upcase}_PROXY"] + proxy = URI.parse(proxy) if String === proxy + excludes = (ENV["NO_PROXY"] || "").split(/\s*,\s*/).compact + excludes = excludes.map { |exclude| exclude =~ /:\d+$/ ? exclude : "#{exclude}:*" } + return proxy unless excludes.any? { |exclude| File.fnmatch(exclude, "#{host}:#{port}") } + end + + def write_internal(options, &block) #:nodoc: + fail "This protocol doesn't support writing (yet, how about helping by implementing it?)" + end + end + + class HTTP #:nodoc: + + # See URI::Generic#read + def read(options = nil, &block) + options ||= {} + connect do |http| + puts "Requesting #{self}" if Rake.application.options.verbose + headers = { 'If-Modified-Since' => CGI.rfc1123_date(options[:modified].utc) } if options[:modified] + request = Net::HTTP::Get.new(request_uri.empty? ? '/' : request_uri, headers) + request.basic_auth self.user, self.password if self.user + http.request request do |response| + case response + when Net::HTTPNotModified + # No modification, nothing to do. + puts 'Not modified since last download' if Rake.application.options.verbose + return nil + when Net::HTTPRedirection + # Try to download from the new URI, handle relative redirects. + puts "Redirected to #{response['Location']}" if Rake.application.options.verbose + return (self + URI.parse(response['location'])).read(options, &block) + when Net::HTTPOK + puts "Downloading #{self}" if Rake.application.options.verbose + result = nil + with_progress_bar options[:progress], path.split('/').last, response.content_length do |progress| + if block + response.read_body do |chunk| + block.call chunk + progress << chunk + end + else + result = '' + response.read_body do |chunk| + result << chunk + progress << chunk + end + end + end + return result + when Net::HTTPNotFound + raise NotFoundError, "Looking for #{self} and all I got was a 404!" + else + raise RuntimeError, "Failed to download #{self}: #{response.message}" + end + end + end + end + + private + + def connect + if proxy = proxy_uri + proxy = URI.parse(proxy) if String === proxy + http = Net::HTTP.new(host, port, proxy.host, proxy.port, proxy.user, proxy.password) + else + http = Net::HTTP.new(host, port) + end + if self.instance_of? URI::HTTPS + cacert = "downloads/#{RubyInstaller::Certificate.file}" + http.use_ssl = true + if File.exist?(cacert) + http.ca_file = cacert + else + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + end + yield http + end + end +end diff --git a/tasks/rake/downloadtask.rb b/tasks/rake/downloadtask.rb new file mode 100644 index 000000000..559fa5a62 --- /dev/null +++ b/tasks/rake/downloadtask.rb @@ -0,0 +1,58 @@ +# +# This Rake Task is a striped down version of Buildr download task +# I took the code from http://rubyforge.org/projects/buildr +# +# I've striped down dependencies on Net::SSH and Facets to +# stay as simple as possible. +# +# Original code from Assaf Arkin in the buildr project, released under Apache +# License [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) +# +# Licensed to Puppet Labs under one or more contributor license agreements. +# See the NOTICE file distributed with this work for additional information +# regarding copyright ownership. Puppet Labs licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may not use this file +# except in compliance with the License. You may obtain a copy of the License +# at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +require 'rake' +require 'tempfile' + +require File.join(File.dirname(__FILE__), 'contrib', 'uri_ext') + +def download(args) + args = URI.parse(args) if args.is_a?(String) + + options = { + :progress => true, + :verbose => Rake.application.options.trace + } + + # Given only a download URL, download into a temporary file. + # You can infer the file from task name. + if URI === args + temp = Tempfile.open(File.basename(args.to_s)) + file_create(temp.path) do |task| + # Since temporary file exists, force a download. + class << task ; def needed?() ; true ; end ; end + task.sources << args + task.enhance { args.download(temp, options) } + end + else + # Download to a file task instead + fail unless args.keys.size == 1 + uri = URI.parse(args.values.first.to_s) + file_create(args.keys.first) do |task| + task.sources << uri + task.enhance { uri.download(task.name, options) } + end + end +end diff --git a/tasks/windows/README.markdown b/tasks/windows/README.markdown new file mode 100644 index 000000000..bae55d340 --- /dev/null +++ b/tasks/windows/README.markdown @@ -0,0 +1,50 @@ +# For the Win # + +This project is a small set of Rake tasks to automate the process of building +MSI packages for Puppet on Windows systems. + +This is a separate repository because it is meant to build MSI packages for +arbitrary versions of Puppet, Facter and other related tools. + +This project is meant to be checked out into a special Puppet Windows Dev Kit +directory structure. This Dev Kit will provide the tooling necessary to +actually build the packages. + +This project requires these tools from the `puppetbuilder` Dev Kit for Windows +systems. + + * Ruby + * Rake + * Git + * 7zip + * WiX + +# Getting Started # + +Given a basic Windows 2003 R2 x64 system with the [Puppet Win +Builder](http://links.puppetlabs.com/puppetwinbuilder) archive unpacked into +`C:/puppetwinbuilder/` the following are all that is required to build the MSI +packages. + + C:\>cd puppetwinbuilder + C:\puppetwinbuilder\> build + ... + +(REVISIT - This is the thing we're working to. Make sure this is accurate once +implemented) + +# Making Changes # + +The [Puppet Win Builder](http://links.puppetlabs.com/puppetwinbuilder) archive +should remain relatively static. The purpose of this archive is simply to +bootstrap the tools required for the build process. + +Changes to the build process itself should happen in the [Puppet For the +Win](https://github.com/puppetlabs/puppet_for_the_win) repository on Github. + +# Continuous Integration # + +The `build.bat` build script _should_ work just fine with a build system like +Jenkins. If it does not, please let us know. + +EOF diff --git a/tasks/windows/README_DEVELOPER.markdown b/tasks/windows/README_DEVELOPER.markdown new file mode 100644 index 000000000..89db77ef3 --- /dev/null +++ b/tasks/windows/README_DEVELOPER.markdown @@ -0,0 +1,20 @@ +# Setup Tips # + +To get a shared filesystem: + + net use Z: "\\vmware-host\Shared Folders" /persistent:yes + +# Common Issues # + +I seem to be getting this a lot downloading files: + + undefined method `zero?' for nil:NilClass + +This appears to be from the call to the progress bar method having nil content +from the response.content\_length here: + + % ack with_progress_bar + rake/contrib/uri_ext.rb + 161: # with_progress_bar(enable, file_name, size) { |progress| ... } + 171: def with_progress_bar(enable, file_name, size) #:nodoc: + 254: with_progress_bar options[:progress], path.split('/').last, response.content_length do |progress| diff --git a/tasks/windows/windows.rake b/tasks/windows/windows.rake new file mode 100644 index 000000000..ed554bc14 --- /dev/null +++ b/tasks/windows/windows.rake @@ -0,0 +1,149 @@ +#! /usr/bin/env ruby + +# This rakefile is meant to be run from within the [Puppet Win +# Builder](http://links.puppetlabs.com/puppetwinbuilder) tree. + +# Load Rake +begin + require 'rake' +rescue LoadError + require 'rubygems' + require 'rake' +end + +require 'rake/clean' + +# Added download task from buildr +require 'rake/downloadtask' + +# Where we're situated in the filesystem relative to the Rakefile +TOPDIR=File.expand_path(File.join(File.dirname(__FILE__), "..", "..")) + +# Produce a wixobj from a wxs file. +def candle(wxs_file, basedir) + Dir.chdir File.join(TOPDIR, File.dirname(wxs_file)) do + sh "candle -dStageDir=#{basedir} #{File.basename(wxs_file)}" + end +end + +# Produce a wxs file from a directory in the stagedir +# e.g. heat('wxs/fragments/foo.wxs', 'stagedir/sys/foo') +def heat(wxs_file, stage_dir) + Dir.chdir TOPDIR do + cg_name = File.basename(wxs_file.ext('')) + dir_ref = File.basename(File.dirname(stage_dir)) + sh "heat dir #{stage_dir} -v -ke -indent 2 -cg #{cg_name} -gg -dr #{dir_ref} -var var.StageDir -out #{wxs_file}" + end +end + +def unzip(zip_file, dir) + Dir.chdir TOPDIR do + Dir.chdir dir do + sh "7za -y x #{File.join(TOPDIR, zip_file)}" + end + end +end + +def gitclone(target, uri) + Dir.chdir(File.dirname(target)) do + sh "git clone #{uri} #{File.basename(target)}" + end +end + +CLOBBER.include('downloads/*') +CLEAN.include('stagedir/*') +CLEAN.include('wix/fragments/*.wxs') +CLEAN.include('wix/**/*.wixobj') +CLEAN.include('pkg/*') + +namespace :windows do + + # These are file tasks that behave like mkdir -p + directory 'pkg' + directory 'downloads' + directory 'stagedir/sys' + directory 'wix/fragments' + + ## File Lists + + TARGETS = FileList['pkg/puppet.msi'] + + # These translate to ZIP files we'll download + # FEATURES = %w{ ruby git wix misc } + FEATURES = %w{ ruby } + DOWNLOADS = FEATURES.collect { |fn| File.join("downloads", fn.ext('zip')) } + + # There is a 1:1 mapping between a wxs file and a wixobj file + + # These files should be committed to VCS + WXSFILES = FileList['wix/*.wxs'] + # These files should be auto-generated by heat + WXSFRAGMENTS = FEATURES.collect { |fn| File.join("wix", "fragments", fn.ext('wxs')) } + # All of the objects we need to create + WIXOBJS = (WXSFILES + WXSFRAGMENTS).ext('wixobj') + + # These directories should be unpacked into stagedir/sys + SYSTOOLS = FEATURES.collect { |fn| File.join("stagedir", "sys", fn) } + + task :default => :build + # High Level Tasks. Other tasks will add themselves to these tasks + # dependencies. + + # This is also called from the build script in the Puppet Win Builder archive. + # This will be called AFTER the update task in a new process. + desc "Build puppet.msi" + task :build => "pkg/puppet.msi" + + desc "Download example" + task :download => DOWNLOADS + + desc "Unzip and stage sys tools" + task :unzip => SYSTOOLS + + desc "List available rake tasks" + task :help do + sh 'rake -T' + end + + # The update task is always called from the build script + # This gives the repository an opportunity to update itself + # and manage how it updates itself. + desc "Update the build scripts" + task :update do + sh 'git pull' + end + + # Tasks to unpack the zip files + SYSTOOLS.each do |systool| + zip_file = File.join("downloads", File.basename(systool).ext('zip')) + file systool => [ zip_file, File.dirname(systool) ] do + unzip(zip_file, File.dirname(systool)) + end + end + + DOWNLOADS.each do |fn| + file fn => [ File.dirname(fn) ] do |t| + download t.name => "http://downloads.puppetlabs.com/development/ftw/#{File.basename(t.name)}" + end + end + + WIXOBJS.each do |wixobj| + stagedir = File.join(TOPDIR, 'stagedir', 'sys') + file wixobj => [ wixobj.ext('wxs'), File.dirname(wixobj) ] do |t| + candle t.name.ext('wxs'), File.join(stagedir, File.basename(t.name.ext(''))).gsub('/', File::SEPARATOR) + end + end + + WXSFRAGMENTS.each do |wxs_frag| + source_dir = File.join('stagedir', 'sys', File.basename(wxs_frag).ext('')) + file wxs_frag => [ source_dir, File.dirname(wxs_frag) ] do |t| + heat t.name, source_dir + end + end + + ####### REVISIT + + file 'pkg/puppet.msi' => WIXOBJS do |t| + sh "light #{t.prerequisites.join(' ')} -out #{t.name}" + end +end diff --git a/wix/include/puppet.wxi b/wix/include/puppet.wxi new file mode 100644 index 000000000..f1af4de54 --- /dev/null +++ b/wix/include/puppet.wxi @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/wix/puppet.wxs b/wix/puppet.wxs new file mode 100644 index 000000000..48e801245 --- /dev/null +++ b/wix/puppet.wxs @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +