Class: Vanagon::Platform::Windows

Inherits:
Vanagon::Platform show all
Defined in:
lib/vanagon/platform/windows.rb

Constant Summary

Constants inherited from Vanagon::Platform

PLATFORM_REGEX, VERSION_REGEX

Instance Attribute Summary

Attributes inherited from Vanagon::Platform

#abs_resource_name, #architecture, #aws_ami, #aws_instance_type, #aws_key, #aws_key_name, #aws_region, #aws_shutdown_behavior, #aws_subnet_id, #aws_user_data, #aws_vpc_id, #build_dependencies, #build_hosts, #cflags, #codename, #copy, #cross_compiled, #defaultdir, #dist, #docker_image, #docker_run_args, #environment, #find, #install, #ldflags, #make, #mktemp, #name, #num_cores, #os_name, #os_version, #output_dir, #package_type, #patch, #platform_triple, #provisioning, #rpmbuild, #sed, #servicedir, #servicetype, #servicetypes, #settings, #shasum, #shell, #sort, #source_output_dir, #ssh_port, #tar, #target_user, #use_docker_exec, #valid_operators, #vmpooler_template

Instance Method Summary collapse

Methods inherited from Vanagon::Platform

#[], #add_build_repository, #add_group, #add_user, #generate_compiled_archive, #get_service_dir, #get_service_types, #is_aix?, #is_cisco_wrlinux?, #is_cross_compiled?, #is_cross_compiled_linux?, #is_deb?, #is_debian?, #is_el8?, #is_el?, #is_eos?, #is_fedora?, #is_fips?, #is_huaweios?, #is_linux?, #is_macos?, #is_osx?, #is_rpm?, #is_sles?, #is_solaris?, #is_ubuntu?, #is_unix?, #is_windows?, load_platform, #package_override, #provision_with, #validate_operator, #version_munger

Methods included from HashableAttributes

#to_hash, #to_json

Methods included from Utilities

#erb_file, #erb_string, #ex, #find_program_on_path, #get_md5sum, #get_sum, #http_request, #http_request_code, #http_request_generic, #local_command, #remote_ssh_command, #retry_with_timeout, #rsync_from, #rsync_to, #ssh_command

Constructor Details

#initialize(name) ⇒ Vanagon::Platform::Windows

Constructor. Sets up some defaults for the windows platform and calls the parent constructor

Mingw varies on where it is installed based on architecture. We want to use which ever is on the system.

Parameters:

  • name (String)

    name of the platform



450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/vanagon/platform/windows.rb', line 450

def initialize(name)
  @target_user = "Administrator"
  @make = "/usr/bin/make"
  @tar = "/usr/bin/tar"
  @find = "/usr/bin/find"
  @sort = "/usr/bin/sort"
  @num_cores = "/usr/bin/nproc"
  @install = "/usr/bin/install"
  @copy = "/usr/bin/cp"
  @package_type = "msi"
  super(name)
end

Instance Method Details

#add_repository(definition) ⇒ Array

Add a repository (or install Chocolatey)

Note - this only prepares the list of commands to be executed once the Platform has been setup

Parameters:

  • definition (String)

    Definition for adding repo, can be a Repo URL (including file:) If file suffix is ‘ps1’ it is downloaded and executed to install chocolatey

Returns:

  • (Array)

    Commands to executed after platform startup



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/vanagon/platform/windows.rb', line 314

def add_repository(definition)
  definition = URI.parse(definition)
  commands = []

  if definition.scheme =~ /^(http|ftp|file)/
    if File.extname(definition.path) == '.ps1'
      commands << %(powershell.exe -NoProfile -ExecutionPolicy Bypass -Command 'iex ((new-object net.webclient).DownloadString(\"#{definition}\"))')
    else
      commands << %(C:/ProgramData/chocolatey/bin/choco.exe source add -n #{definition.host}-#{definition.path.tr('/', '-')} -s "#{definition}" --debug || echo "Oops, it seems that you don't have chocolatey installed on this system. Please ensure it's there by adding something like 'plat.add_repository 'https://chocolatey.org/install.ps1'' to your platform definition.")
    end
  else
    raise Vanagon::Error, "Invalid repo specification #{definition}"
  end

  commands
end

#copy_from_project(proj_resources, destination, verbose: false) ⇒ Object

Method to recursively copy from a source project resource directory to a destination (wix) work directory.

strongly suspect the original cp_r command would have done all of this.

Parameters:

  • proj_resources (String)

    Project Resource File directory

  • destination (String)

    Destination directory

  • verbose (String) (defaults to: false)

    True or false



82
83
84
# File 'lib/vanagon/platform/windows.rb', line 82

def copy_from_project(proj_resources, destination, verbose: false)
  FileUtils.cp_r(proj_resources, destination, :verbose => verbose)
end

#generate_msi_package(project) ⇒ Array

The specific bits used to generate a windows msi package for a given project

Have changed this to reflect the overall commands we need to generate the package. Question - should we break this down into some simpler Make tasks ?

1. Heat the directory tree to produce the file list
2. Compile (candle) all the wxs files into wixobj files
3. Run light to produce the final MSI

Parameters:

Returns:

  • (Array)

    list of commands required to build an msi package for the given project from a tarball



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/vanagon/platform/windows.rb', line 181

def generate_msi_package(project) # rubocop:disable Metrics/AbcSize
  target_dir = project.repo ? output_dir(project.repo) : output_dir
  wix_extensions = "-ext WiXUtilExtension -ext WixUIExtension"
  # Heat command documentation at: http://wixtoolset.org/documentation/manual/v3/overview/heat.html
  #   dir <directory> - Traverse directory to find all sub-files and directories.
  #   -ke             - Keep Empty directories
  #   -cg             - Component Group Name
  #   -gg             - Generate GUIDS now
  #   -srd            - Suppress root element generation, we want to reference one of the default root elements
  #                     INSTALLDIR or APPDATADIR in the directorylist.wxs file, not a newly generated one.
  #   -sreg           - Suppress registry harvesting.
  #   -dr             - Root DirectoryRef to point all components to
  #   -var            - Replace "SourceDir" in the @source attributes of all components with a preprocessor variable
  app_heat_flags = " -dr INSTALLDIR -v -ke -indent 2 -cg AppComponentGroup -gg -srd -t wix/filter.xslt -sreg -var var.AppSourcePath "
  app_heat_flags += " -fips" if project.platform.name =~ /windowsfips-2012r2/

  app_source_path = "SourceDir/#{project.settings[:base_dir]}/#{project.settings[:company_id]}/#{project.settings[:product_id]}"
  # Candle.exe preprocessor vars are required due to the above double run of heat.exe, both runs of heat use
  # preprocessor variables
  candle_preprocessor = "-dAppSourcePath=\"#{app_source_path}\" "
  candle_flags = "-arch #{@architecture} #{wix_extensions}"
  candle_flags += " -fips" if project.platform.name =~ /windowsfips-2012r2/

  # Enable verbose mode for the moment (will be removed for production)
  # localisation flags to be added
  light_flags = "-v -cultures:en-us #{wix_extensions}"
  # "Misc Dir for versions.txt, License file and Icon file"
  misc_dir = "SourceDir/#{project.settings[:base_dir]}/#{project.settings[:company_id]}/#{project.settings[:product_id]}/misc"
  # Actual array of commands to be written to the Makefile
  make_commands = [
    "mkdir -p output/#{target_dir}",
    "mkdir -p $(tempdir)/{SourceDir,wix/wixobj}",
    "#{@copy} -r wix/* $(tempdir)/wix/",
    "gunzip -c #{project.name}-#{project.version}.tar.gz | '#{@tar}' -C '$(tempdir)/SourceDir' --strip-components 1 -xf -"
  ]

  if project.extra_files_to_sign.any?
    make_commands << Vanagon::Utilities::ExtraFilesSigner.commands(project, @mktemp, 'SourceDir')
  end

  make_commands << [
    "mkdir -p $(tempdir)/#{misc_dir}",
    # Need to use awk here to convert to DOS format so that notepad can display file correctly.
    "awk 'sub(\"$$\", \"\\r\")' $(tempdir)/SourceDir/bill-of-materials > $(tempdir)/#{misc_dir}/versions.txt",
    "cd $(tempdir); \"$$WIX/bin/heat.exe\" dir #{app_source_path} #{app_heat_flags} -out wix/#{project.name}-harvest-app.wxs",

    # Apply Candle command to all *.wxs files - generates .wixobj files in wix directory.
    # cygpath conversion is necessary as candle is unable to handle posix path specs
    # the preprocessor variables AppDataSourcePath and ApplicationSourcePath are required due to the -var input to the heat
    # runs listed above.
    "cd $(tempdir)/wix/wixobj; for wix_file in `find $(tempdir)/wix -name \'*.wxs\'`; do \"$$WIX/bin/candle.exe\" #{candle_flags} #{candle_preprocessor} $$(cygpath -aw $$wix_file) || exit 1; done",
    # run all wix objects through light to produce the msi
    # the -b flag simply points light to where the SourceDir location is
    # -loc is required for the UI localization it points to the actual localization .wxl
    "cd $(tempdir)/wix/wixobj; \"$$WIX/bin/light.exe\" #{light_flags} -b $$(cygpath -aw $(tempdir)) -loc $$(cygpath -aw $(tempdir)/wix/localization/puppet_en-us.wxl) -out $$(cygpath -aw $(workdir)/output/#{target_dir}/#{msi_package_name(project)}) *.wixobj",
  ]

  make_commands.flatten
end

#generate_msi_packaging_artifacts(workdir, name, binding) ⇒ Object

Method to generate the files required to build an MSI package for the project

Parameters:

  • workdir (String)

    working directory to stage the evaluated templates in

  • name (String)

    name of the project

  • binding (Binding)

    binding to use in evaluating the packaging templates



67
68
69
70
71
72
# File 'lib/vanagon/platform/windows.rb', line 67

def generate_msi_packaging_artifacts(workdir, name, binding)
  # Copy the project specific files first
  copy_from_project("./resources/windows/wix", workdir)
  merge_defaults_from_vanagon(File.join(VANAGON_ROOT, "resources/windows/wix"), "#{workdir}/wix")
  process_templates("#{workdir}/wix", binding)
end

#generate_nuget_package(project) ⇒ Array

The specific bits used to generate a windows nuget package for a given project

Nexus expects packages to be named ‘#Vanagon::Platform#name-#version.nupkg`. However, chocolatey will generate them to be `#Vanagon::Platform#name.#version.nupkg`. So, we have to rename the package after we build it.

Parameters:

Returns:

  • (Array)

    list of commands required to build a nuget package for the given project from a tarball



158
159
160
161
162
163
164
165
166
167
168
# File 'lib/vanagon/platform/windows.rb', line 158

def generate_nuget_package(project) # rubocop:disable Metrics/AbcSize
  target_dir = project.repo ? output_dir(project.repo) : output_dir
  ["mkdir -p output/#{target_dir}",
  "mkdir -p $(tempdir)/#{project.name}/tools",
  "#{@copy} #{project.name}.nuspec $(tempdir)/#{project.name}/",
  "#{@copy} chocolateyInstall.ps1 chocolateyUninstall.ps1 $(tempdir)/#{project.name}/tools/",
  "#{@copy} file-list $(tempdir)/#{project.name}/tools/file-list.txt",
  "gunzip -c #{project.name}-#{project.version}.tar.gz | '#{@tar}' -C '$(tempdir)/#{project.name}/tools' --strip-components 1 -xf -",
  "(cd $(tempdir)/#{project.name} ; C:/ProgramData/chocolatey/bin/choco.exe pack #{project.name}.nuspec)",
  "#{@copy} $(tempdir)/#{project.name}/#{project.name}-#{@architecture}.#{nuget_package_version(project.version, project.release)}.nupkg ./output/#{target_dir}/#{nuget_package_name(project)}"]
end

#generate_nuget_packaging_artifacts(workdir, name, binding) ⇒ Object

Method to generate the files required to build a nuget package for the project

Parameters:

  • workdir (String)

    working directory to stage the evaluated templates in

  • name (String)

    name of the project

  • binding (Binding)

    binding to use in evaluating the packaging templates



139
140
141
142
143
144
145
146
147
# File 'lib/vanagon/platform/windows.rb', line 139

def generate_nuget_packaging_artifacts(workdir, name, binding)
  # nuget templates that do require a name change
  erb_file(File.join(VANAGON_ROOT, "resources/windows/nuget/project.nuspec.erb"), File.join(workdir, "#{name}.nuspec"), false, { :binding => binding })

  # nuget static resources to be copied into place
  ["chocolateyInstall.ps1", "chocolateyUninstall.ps1"].each do |win_file|
    FileUtils.copy(File.join(VANAGON_ROOT, "resources/windows/nuget/#{win_file}"), File.join(workdir, win_file))
  end
end

#generate_package(project) ⇒ Array

The specific bits used to generate a windows package for a given project

Parameters:

Returns:

  • (Array)

    list of commands required to build a windows package for the given project from a tarball



10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/vanagon/platform/windows.rb', line 10

def generate_package(project)
  case project.platform.package_type
  when "msi"
    return generate_msi_package(project)
  when "nuget"
    return generate_nuget_package(project)
  when "archive"
    # We don't need to generate the package for archives, return an
    # empty array
    return []
  else
    raise Vanagon::Error, "I don't know how to build package type '#{project.platform.package_type}' for Windows. Teach me?"
  end
end

#generate_packaging_artifacts(workdir, name, binding, project) ⇒ Object

Method to generate the files required to build a windows package for the project

Parameters:

  • workdir (String)

    working directory to stage the evaluated templates in

  • name (String)

    name of the project

  • binding (Binding)

    binding to use in evaluating the packaging templates

  • project (Vanagon::Project)

    Vanagon::Project we are building for



48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/vanagon/platform/windows.rb', line 48

def generate_packaging_artifacts(workdir, name, binding, project)
  case @package_type
  when "msi"
    return generate_msi_packaging_artifacts(workdir, name, binding)
  when "nuget"
    return generate_nuget_packaging_artifacts(workdir, name, binding)
  when "archive"
    # We don't need to generate packaging artifacts if this is an archive
    return
  else
    raise Vanagon::Error, "I don't know how create packaging artifacts for package type '#{project.platform.package_type}' for Windows. Teach me?"
  end
end

#generate_service_bin_dirs(services, project) ⇒ Object

Generate the underlying directory structure of any binary files referenced in services.

Note that this function does not generate the structure of the installation directory, only the structure above it.

Parameters:

  • services

    list of all services in a project

  • project

    actual project we are creating the directory structure for



339
340
341
342
343
344
345
346
347
348
# File 'lib/vanagon/platform/windows.rb', line 339

def generate_service_bin_dirs(services, project)
  # All service files will need a directory reference
  items = services.map do |svc|
    {
      :path => strip_and_format_path(svc.service_file, project),
      :element_to_add => "<Directory Id=\"#{svc.bindir_id}\" />\n"
    }
  end
  generate_wix_dirs(items)
end

#generate_wix_dirs(items) ⇒ string

Generate correctly formatted wix elements that match the structure of the itemized input

Parameters:

  • items

Returns:

  • (string)

    correctly formatted wix element string



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/vanagon/platform/windows.rb', line 355

def generate_wix_dirs(items)
  # root refers to the root of an n-ary tree (which we are about to make)
  root = { :children => [] }
  # iterate over all paths specified and break each one
  # in to its specific directories. This will generate_wix_dirs
  # an n-ary tree structure matching the specs from the input
  items.each_with_index do |item, item_idx|
    # Always start at the beginning
    curr = root
    names = item[:path].split(File::SEPARATOR)
    names.each_with_index do |name, names_idx|
      # We concat the indexes of each loop to name to ensure the ids of all
      # elements will be unique.
      curr = insert_child(curr, name, "#{name}_#{item_idx}_#{names_idx}")
    end
    # at this point, curr will be the top dir, override the id if
    # id exists
    curr[:elements_to_add].push(item[:element_to_add])
  end
  generate_wix_from_graph(root)
end

#generate_wix_from_graph(root) ⇒ Object

Recursively generate wix element structure

Parameters:

  • root,

    the (empty) root of an n-ary tree containing the structure of directories



418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/vanagon/platform/windows.rb', line 418

def generate_wix_from_graph(root)
  string = ''
  unless root[:children].empty?
    root[:children].each do |child|
      string += "<Directory Name=\"#{child[:name]}\" Id=\"#{child[:id]}\">\n"
      unless child[:elements_to_add].empty?
        child[:elements_to_add].each do |element|
          string += element
        end
      end
      string += generate_wix_from_graph(child)
      string += "</Directory>\n"
    end
    return string
  end
  string
end

#index_of_child(new_child, old_children) ⇒ Object

Find if child element is the same as one of the old_children elements, return that child



409
410
411
412
# File 'lib/vanagon/platform/windows.rb', line 409

def index_of_child(new_child, old_children)
  return nil if old_children.empty?
  old_children.index { |child| child[:name] == new_child[:name] }
end

#insert_child(curr, name, id) ⇒ Object

insert a new object with the name “name” if it doesn’t already exist. Then assign curr to either the new child or the one that already exists here

Parameters:

  • curr, (HASH)

    current object we are on

  • name, (string)

    name of new object we are to search for and create if necessary



384
385
386
387
388
389
390
391
392
393
394
# File 'lib/vanagon/platform/windows.rb', line 384

def insert_child(curr, name, id)
  #The Id field will default to name, but be overridden later
  new_obj = { :name => name, :id => id, :elements_to_add => [], :children => [] }
  if (child_index = index_of_child(new_obj, curr[:children]))
    curr = curr[:children][child_index]
  else
    curr[:children].push(new_obj)
    curr = new_obj
  end
  curr
end

#merge_defaults_from_vanagon(vanagon_root, destination, verbose: false) ⇒ Object

Method to merge in the files from the Vanagon (generic) directories.

Project specific files take precedence, so since these are copied prior to this function, then this merge operation will ignore existing files

Parameters:

  • vanagon_root (String)

    Vanagon wix resources directory

  • destination (String)

    Destination directory

  • verbose (String) (defaults to: false)

    True or false



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/vanagon/platform/windows.rb', line 94

def merge_defaults_from_vanagon(vanagon_root, destination, verbose: false) # rubocop:disable Metrics/AbcSize
  # Will use this Pathname object for relative path calculations in loop below.
  vanagon_path = Pathname.new(vanagon_root)
  files = Dir.glob(File.join(vanagon_root, "**/*.*"))
  files.each do |file|
    # Get Pathname for incoming file using Pathname library
    src_pathname = Pathname.new(file).dirname
    # This Pathname method allows us to effectively "subtract" the leading vanagon_path
    # from the source filename path. This gives us a pathname fragment that we can
    # then append to the target directory, preserving the files place in the directory
    # tree relative to the parent.
    # See following article for example:
    # http://stackoverflow.com/questions/12093770/ruby-removing-parts-a-file-path
    # and http://ruby-doc.org/stdlib-2.1.0/libdoc/pathname/rdoc/Pathname.html#method-i-relative_path_from
    dest_pathname_fragment = src_pathname.relative_path_from(vanagon_path)
    target_dir = File.join(destination, dest_pathname_fragment.to_s)
    # Create the target directory if necessary.
    FileUtils.mkdir_p(target_dir) unless File.exists?(target_dir)
    # Skip the file copy if either target file or ERB equivalent exists.
    # This means that any files already in place in the work directory as a
    # result of being copied from the project specific area will not be
    # overritten.
    next if File.exists?(Pathname.new(target_dir) + File.basename(file))
    next if File.exists?(Pathname.new(target_dir) + File.basename(file, ".erb"))
    FileUtils.cp(file, target_dir, :verbose => verbose)
  end
end

#msi_package_name(project) ⇒ String

Method to derive the msi (Windows Installer) package name for the project.

Parameters:

Returns:

  • (String)

    name of the windows package for this project



245
246
247
248
# File 'lib/vanagon/platform/windows.rb', line 245

def msi_package_name(project)
  # Decided to use native project version in hope msi versioning doesn't have same resrictions as nuget
  "#{project.name}-#{project.version}-#{@architecture}.msi"
end

#nuget_package_name(project) ⇒ String

Method to derive the package name for the project.

Neither chocolatey nor nexus know how to deal with architecture, so we are just pretending it’s part of the package name.

Parameters:

Returns:

  • (String)

    name of the windows package for this project



257
258
259
# File 'lib/vanagon/platform/windows.rb', line 257

def nuget_package_name(project)
  "#{project.name}-#{@architecture}-#{nuget_package_version(project.version, project.release)}.nupkg"
end

#nuget_package_version(version, release) ⇒ String

Nuget versioning is awesome!

Nuget and chocolatey have some expectations about version numbers.

First, if this is a final package (built from a tag), the version must only contain digits with each element of the version separated by periods.

If we are creating the version for a prerelease package (built from a commit that does not have a corresponding tag), we have the option to append a string to the version. The string must start with a letter, be separated from the rest of the version with a dash, and contain no punctuation.

We assume we are working from a semver tag as the base of our version. If this is a final release, we only have to worry about that tag. We can also include the release number in the package version. If this is a prerelease package, then we assume we have a semver compliant tag, followed by the number of commits beyond the tag and the short sha of the latest change. Because we are working with git, if the version contains a short sha, it will begin with ‘g’. We check for this to determine what version type to deliver.

Examples of final versions:

* 1.2.3
* 1.5.3.1

Examples of prerelease versions:

* 1.2.3.1234-g124dm9302
* 3.2.5.23-gd329nd

Parameters:

Returns:

  • (String)

    the version of the nuget package for this project



296
297
298
299
300
301
302
303
304
# File 'lib/vanagon/platform/windows.rb', line 296

def nuget_package_version(version, release)
  version_elements = version.split('.')
  if version_elements.last.start_with?('g')
    # Version for the prerelease package
    "#{version_elements[0..-2].join('.').gsub(/[a-zA-Z]/, '')}-#{version_elements[-1]}"
  else
    "#{version}.#{release}".gsub(/[a-zA-Z]/, '')
  end
end

#package_name(project) ⇒ String

Method to derive the package name for the project

Parameters:

Returns:

  • (String)

    name of the windows package for this project



29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/vanagon/platform/windows.rb', line 29

def package_name(project)
  case project.platform.package_type
  when "msi"
    return msi_package_name(project)
  when "nuget"
    return nuget_package_name(project)
  when "archive"
    return "#{project.name}-#{project.version}-archive"
  else
    raise Vanagon::Error, "I don't know how to name package type '#{project.platform.package_type}' for Windows. Teach me?"
  end
end

#process_templates(wixworkdir, binding) ⇒ Object

Method to transform ERB templates in the work directory.

Parameters:

  • workdir (String)

    working directory to stage the evaluated templates in

  • binding (Binding)

    binding to use in evaluating the packaging templates



126
127
128
129
130
131
132
# File 'lib/vanagon/platform/windows.rb', line 126

def process_templates(wixworkdir, binding)
  files = Dir.glob(File.join(wixworkdir, "**/*.erb"))
  files.each do |file|
    erb_file(file, File.join(File.dirname(file), File.basename(file, ".erb")), false, { :binding => binding })
    FileUtils.rm(file)
  end
end

#strip_and_format_path(path, project) ⇒ Object

strip the leading install root and the filename from the service path and replace any \ with /

Parameters:

  • path (string)

    string of directory

  • project (@project)

    object



401
402
403
404
405
# File 'lib/vanagon/platform/windows.rb', line 401

def strip_and_format_path(path, project)
  formatted_path = path.tr('\\', '\/')
  path_regex = /\/?SourceDir\/#{project.settings[:base_dir]}\/#{project.settings[:company_id]}\/#{project.settings[:product_id]}\//
  File.dirname(formatted_path.sub(path_regex, ''))
end

#wix_product_version(version) ⇒ Object

Grab only the first three values from the version input and strip off any non-digit characters.

Parameters:

  • version, (string)

    the original version number



440
441
442
# File 'lib/vanagon/platform/windows.rb', line 440

def wix_product_version(version)
  version.split("\.").first(3).collect { |value| value.gsub(/[^0-9]/, '') }.join("\.")
end