reflex-packager 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,183 @@
1
+ require 'yaml'
2
+
3
+
4
+ module Reflex
5
+
6
+
7
+ # Tools to package a Reflex application as a distributable bundle.
8
+ # Files under this directory must not require 'reflex' because it has
9
+ # side effects such as requiring native extensions.
10
+ #
11
+ module Packager
12
+
13
+
14
+ # Application project configuration loaded from 'reflex.yml'.
15
+ #
16
+ class Config
17
+
18
+ def self.defaults(profile, dir, hash)
19
+ name = hash[:name]&.to_s || File.basename(dir)
20
+ {
21
+ name: name,
22
+ bundle_id: hash[:bundle_id] || default_bundle_id(profile, name),
23
+ version: '0.1.0',
24
+ main: 'main.rb',
25
+ icon: nil,
26
+ files: nil,
27
+ pods: {
28
+ :cruby => {tag: nil, branch: nil, git: nil, path: nil},
29
+ profile.pod_key => {tag: nil, branch: nil, git: nil, path: nil}
30
+ },
31
+ macos: MacOSConfig.defaults
32
+ }
33
+ end
34
+
35
+ # Load the config file in the project directory.
36
+ #
37
+ # @param [Profile] profile runtime profile to package for
38
+ # @param [String] dir project directory
39
+ # @param [String] path config file path to use instead of the default
40
+ #
41
+ # @return [Config] config object
42
+ #
43
+ def self.load(profile, dir, path = nil)
44
+ raise Error, "config file not found: '#{path}'" if path && !File.file?(path)
45
+ path ||= profile.config_files.map {File.join dir, _1}.find {File.file? _1}
46
+ new profile, dir, (path ? YAML.safe_load(File.read(path), aliases: true) : nil)
47
+ rescue Psych::SyntaxError => e
48
+ raise Error, "failed to parse '#{path}': #{e.message}"
49
+ end
50
+
51
+ def initialize(profile, dir, hash = nil, stderr: $stderr)
52
+ raise Error, "no such directory: '#{dir}'" unless File.directory? dir
53
+
54
+ @profile = profile
55
+ @dir = File.expand_path dir
56
+ hash = symbolize_keys hash || {}
57
+ hash = validate_input hash, Config.defaults(profile, @dir, hash), stderr: stderr
58
+
59
+ @name = hash[:name] .to_s
60
+ @bundle_id = hash[:bundle_id].to_s
61
+ @version = hash[:version] .to_s
62
+ @main = hash[:main] .to_s
63
+ @icon = hash[:icon] &.to_s
64
+ @files = hash[:files]&.then {Array(_1).map(&:to_s)}
65
+ @pods = hash[:pods].transform_values &:compact
66
+ @macos = MacOSConfig.new hash[:macos]
67
+ validate
68
+ end
69
+
70
+ attr_reader :profile, :dir, :name, :bundle_id, :version, :main, :icon,
71
+ :files, :macos, :pods
72
+
73
+ # Returns paths to be bundled into the application, relative to the
74
+ # project directory.
75
+ #
76
+ # @return [Array<String>] relative paths
77
+ #
78
+ def app_files()
79
+ excludes = EXCLUDES + @profile.config_files
80
+ [@main, *@files]
81
+ .flat_map {Dir.glob _1, base: @dir}
82
+ .reject {_1.start_with?('.') || excludes.include?(_1)}
83
+ .uniq
84
+ .sort
85
+ end
86
+
87
+ EXCLUDES = %w[build dist]
88
+
89
+ private
90
+
91
+ def self.default_bundle_id(profile, name)
92
+ id = name.downcase.gsub(/[^a-z0-9\-]+/, '')
93
+ if id.empty?
94
+ raise Error, "cannot derive a bundle_id from name '#{name}', " +
95
+ "set 'bundle_id' in #{profile.config_files.first}"
96
+ end
97
+ "#{profile.bundle_id_prefix}.#{id}"
98
+ end
99
+
100
+ def symbolize_keys(hash)
101
+ hash.map {|k, v|
102
+ [
103
+ k.to_sym,
104
+ v.is_a?(Hash) ? symbolize_keys(v) : v
105
+ ]
106
+ }.to_h
107
+ end
108
+
109
+ def validate_input(hash, defaults, parent: '', stderr: nil)
110
+ return defaults.dup if hash.nil?
111
+ raise Error, "not a Hash: '#{parent}'" unless hash.is_a? Hash
112
+ hash.each_key {stderr&.puts "unknown key '#{parent}/#{_1}'" unless defaults.key? _1}
113
+
114
+ defaults.each.with_object({}) do |(key, defval), result|
115
+ value = hash[key]
116
+ result[key] =
117
+ if defval.is_a? Hash
118
+ validate_input value, defval, parent: "#{parent}/#{key}", stderr: stderr
119
+ else
120
+ raise Error, "unexpected Hash: '#{parent}/#{key}'" if value.is_a? Hash
121
+ value.nil? ? defval : value
122
+ end
123
+ end
124
+ end
125
+
126
+ def validate()
127
+ raise Error, "invalid bundle_id: '#{@bundle_id}'" if
128
+ @bundle_id !~ /\A[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+\z/
129
+
130
+ raise Error, "invalid version: '#{@version}'" if
131
+ @version !~ /\A\d+(\.\d+)*\z/
132
+
133
+ raise Error, "main script not found: '#{@main}'" if
134
+ !File.file?(File.join @dir, @main)
135
+
136
+ raise Error, "icon not found: '#{@icon}'" if
137
+ @icon && !File.file?(File.join @dir, @icon)
138
+ end
139
+
140
+ end# Config
141
+
142
+
143
+ # macOS specific configuration.
144
+ #
145
+ class MacOSConfig
146
+
147
+ def self.defaults()
148
+ {
149
+ deployment_target: '11.0',
150
+ archs: 'arm64',
151
+ codesign: {
152
+ identity: '-',
153
+ team_id: nil
154
+ }
155
+ }
156
+ end
157
+
158
+ def initialize(hash)
159
+ @deployment_target = hash[:deployment_target] .to_s
160
+ @archs = Array(hash[:archs]).map &:to_s
161
+ @codesign_identity = hash[:codesign][:identity].to_s
162
+ @codesign_team_id = hash[:codesign][:team_id]&.to_s
163
+ validate
164
+ end
165
+
166
+ attr_reader :deployment_target, :archs, :codesign_identity, :codesign_team_id
167
+
168
+ def validate()
169
+ raise Error, "invalid archs: '#{@archs}'" if @archs.empty?
170
+ end
171
+
172
+ end# MacOSConfig
173
+
174
+
175
+ # Raised on invalid configuration or packaging failure.
176
+ #
177
+ class Error < StandardError; end
178
+
179
+
180
+ end# Packager
181
+
182
+
183
+ end# Reflex
@@ -0,0 +1,32 @@
1
+ module Reflex
2
+
3
+
4
+ module Packager
5
+
6
+
7
+ # @private
8
+ module Extension
9
+
10
+ module_function
11
+
12
+ def name(downcase = false)
13
+ super().split('::')[..-2].join.then {|s|
14
+ downcase ? s.gsub(/([a-z])([A-Z])/) {"#{$1}-#{$2}"}.downcase : s
15
+ }
16
+ end
17
+
18
+ def version()
19
+ File.read(root_dir 'VERSION')[/[\d\.]+/]
20
+ end
21
+
22
+ def root_dir(path = '')
23
+ File.expand_path "../../../#{path}", __dir__
24
+ end
25
+
26
+ end# Extension
27
+
28
+
29
+ end# Packager
30
+
31
+
32
+ end# Reflex
@@ -0,0 +1,190 @@
1
+ require 'reflex/packager/platform'
2
+
3
+
4
+ module Reflex
5
+
6
+
7
+ module Packager
8
+
9
+
10
+ # Packages a Reflex application as a macOS application bundle.
11
+ #
12
+ class MacOS < Platform
13
+
14
+ GIT_CRUBY = 'https://github.com/xord/cruby'
15
+
16
+ TOOLS = {
17
+ xcodegen: 'install with: brew install xcodegen',
18
+ pod: 'install with: brew install cocoapods (or gem install cocoapods)',
19
+ xcodebuild: 'install Xcode and run: sudo xcode-select --switch /Applications/Xcode.app'
20
+ }
21
+
22
+ def generate()
23
+ copy_app_files
24
+ generate_icon if config.icon
25
+ write 'project.yml', render('project.yml.erb')
26
+ write 'Podfile', render('Podfile.erb')
27
+ write 'src/main.mm', render('main.mm.erb')
28
+ end
29
+
30
+ def build()
31
+ check_tools TOOLS
32
+ check_dev_pods
33
+ run 'xcodegen', 'generate', chdir: build_dir
34
+ run 'pod', 'install', *('--verbose' if verbose?), chdir: build_dir,
35
+ env: {'os' => 'macos'}
36
+ run 'xcodebuild',
37
+ '-workspace', "#{target}.xcworkspace",
38
+ '-scheme', target,
39
+ '-configuration', 'Release',
40
+ '-destination', 'platform=macOS',
41
+ '-derivedDataPath', 'DerivedData',
42
+ 'build',
43
+ chdir: build_dir
44
+ copy_app
45
+ end
46
+
47
+ # Returns the Xcode target name: the app name without characters
48
+ # unsafe for target/scheme/file names.
49
+ #
50
+ def target()
51
+ config.name.gsub(/[^A-Za-z0-9_\-]+/, '').then {_1.empty? ? 'App' : _1}
52
+ end
53
+
54
+ def build_dir()
55
+ File.join config.dir, 'build', platform_name
56
+ end
57
+
58
+ def dist_dir()
59
+ File.join config.dir, 'dist'
60
+ end
61
+
62
+ # Native extensions registered with CRuby (Init_<name> symbols).
63
+ #
64
+ def extensions()
65
+ profile.extensions
66
+ end
67
+
68
+ # Ruby library bundles added to the load path.
69
+ #
70
+ def libraries()
71
+ profile.libraries
72
+ end
73
+
74
+ # Returns {'CRuby' => {...}, '<pod>' => {...}} resolved from the config,
75
+ # the <POD>_PODS_PATH env var, or the defaults.
76
+ #
77
+ def pod_refs()
78
+ p = profile
79
+ {
80
+ 'CRuby' => pod_ref(:cruby, {git: GIT_CRUBY}),
81
+ p.pod => pod_ref(p.pod_key, {git: p.git, tag: "v#{p.version}"})
82
+ }
83
+ end
84
+
85
+ # Returns local directories of pods referenced by path, which need
86
+ # special handling because CocoaPods does not place development
87
+ # pods under PODS_ROOT and does not run their prepare_command.
88
+ #
89
+ def dev_pod_paths()
90
+ pod_refs
91
+ .filter_map {|name, ref| [name, ref[:path]] if ref[:path]}
92
+ .to_h
93
+ end
94
+
95
+ # Returns sips command lines to resize the icon into an iconset.
96
+ #
97
+ def icon_commands(src, iconset_dir)
98
+ [16, 32, 128, 256, 512].flat_map {|size|
99
+ [[size, "icon_#{size}x#{size}.png"], [size * 2, "icon_#{size}x#{size}@2x.png"]]
100
+ }.map {|px, file|
101
+ ['sips', '-z', px.to_s, px.to_s, src, '--out', File.join(iconset_dir, file)]
102
+ }
103
+ end
104
+
105
+ private
106
+
107
+ def platform_name()
108
+ 'macos'
109
+ end
110
+
111
+ def pod_ref(name, default)
112
+ ref = config.pods[name]
113
+ return ref unless ref.nil? || ref.empty?
114
+
115
+ root = ENV["#{profile.pod_key.to_s.upcase}_PODS_PATH"]
116
+ root ? {path: File.expand_path(name.to_s, root)} : default
117
+ end
118
+
119
+ def pod_line(name, ref)
120
+ args = ref.map {|key, value| "#{key}: '#{value}'"}.join ', '
121
+ "pod '#{name}', #{args}"
122
+ end
123
+
124
+ def copy_app_files()
125
+ dir = File.join build_dir, 'app'
126
+ FileUtils.rm_rf dir
127
+ FileUtils.mkdir_p dir
128
+ config.app_files.each do |file|
129
+ dest = File.join dir, file
130
+ FileUtils.mkdir_p File.dirname(dest)
131
+ FileUtils.cp_r File.join(config.dir, file), dest
132
+ end
133
+ end
134
+
135
+ def generate_icon()
136
+ iconset = File.join build_dir, 'AppIcon.iconset'
137
+ FileUtils.rm_rf iconset
138
+ FileUtils.mkdir_p iconset
139
+ icon_commands(File.join(config.dir, config.icon), iconset)
140
+ .each {|cmd| run(*cmd, chdir: build_dir)}
141
+ run 'iconutil', '-c', 'icns', 'AppIcon.iconset', '-o', 'AppIcon.icns',
142
+ chdir: build_dir
143
+ end
144
+
145
+ # Development pods skip prepare_command on pod install, so ensure
146
+ # that the manual setups have been done.
147
+ #
148
+ def check_dev_pods()
149
+ dev_pod_paths.each do |name, path|
150
+ raise Error, "pod directory not found: '#{path}'" unless File.directory? path
151
+ if name == 'CRuby'
152
+ unless File.directory? File.join(path, 'CRuby', 'include')
153
+ raise Error,
154
+ "'#{path}' has no CRuby binary, " +
155
+ "run: cd #{path} && rake download_or_build os=macos"
156
+ end
157
+ else # the umbrella pod (Reflex / RubySketch / ...)
158
+ unless File.directory? File.join(path, 'all')
159
+ raise Error,
160
+ "'#{path}' is not set up for CocoaPods, " +
161
+ "run: cd #{path} && rake -f pod.rake setup"
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ def copy_app()
168
+ app = File.join build_dir, 'DerivedData', 'Build', 'Products', 'Release', "#{target}.app"
169
+ raise Error, "application not found: '#{app}'" unless File.directory? app
170
+
171
+ dist = File.join dist_dir, "#{target}.app"
172
+ FileUtils.rm_rf dist
173
+ FileUtils.mkdir_p dist_dir
174
+ FileUtils.cp_r app, dist
175
+ puts "Created #{dist}"
176
+ end
177
+
178
+ def write(path, content)
179
+ path = File.join build_dir, path
180
+ FileUtils.mkdir_p File.dirname(path)
181
+ File.write path, content
182
+ end
183
+
184
+ end# MacOS
185
+
186
+
187
+ end# Packager
188
+
189
+
190
+ end# Reflex
@@ -0,0 +1,73 @@
1
+ require 'erb'
2
+ require 'fileutils'
3
+
4
+
5
+ module Reflex
6
+
7
+
8
+ module Packager
9
+
10
+
11
+ TEMPLATES_DIR = File.expand_path 'templates', __dir__
12
+
13
+ # Base class for platform specific packagers.
14
+ #
15
+ class Platform
16
+
17
+ def initialize(config, verbose: false)
18
+ @config, @verbose = config, verbose
19
+ end
20
+
21
+ attr_reader :config
22
+
23
+ def profile
24
+ config.profile
25
+ end
26
+
27
+ def verbose?()
28
+ @verbose
29
+ end
30
+
31
+ # Package the application as a distributable bundle.
32
+ #
33
+ # @param [Boolean] generate_only generate the project files but do
34
+ # not build them
35
+ #
36
+ def package(generate_only: false)
37
+ generate
38
+ build unless generate_only
39
+ end
40
+
41
+ private
42
+
43
+ def render(template)
44
+ path = File.join TEMPLATES_DIR, platform_name, template
45
+ ERB.new(File.read(path), trim_mode: '-').result binding
46
+ end
47
+
48
+ def run(*cmd, chdir:, env: {})
49
+ puts "==> #{cmd.join ' '}"
50
+ return if system env, *cmd, chdir: chdir
51
+ raise Error, "command failed: #{cmd.join ' '}"
52
+ end
53
+
54
+ def check_tools(tools)
55
+ missing = tools.reject {|name, _| executable? name}
56
+ return if missing.empty?
57
+
58
+ list = missing.map {|name, hint| ' %-10s -- %s' % [name, hint]}
59
+ raise Error, "required tools not found:\n#{list.join "\n"}"
60
+ end
61
+
62
+ def executable?(name)
63
+ ENV['PATH'].to_s.split(File::PATH_SEPARATOR)
64
+ .any? {|dir| File.executable? File.join(dir, name.to_s)}
65
+ end
66
+
67
+ end# Platform
68
+
69
+
70
+ end# Packager
71
+
72
+
73
+ end# Reflex
@@ -0,0 +1,45 @@
1
+ module Reflex
2
+
3
+
4
+ module Packager
5
+
6
+
7
+ # Describes the runtime a packaged app embeds: the umbrella pod that
8
+ # provides the native build, the extensions and libraries registered with
9
+ # CRuby, and the scaffold for the 'new' command.
10
+ #
11
+ # The packager itself is runtime-agnostic; each gem (reflex, rubysketch,
12
+ # ...) supplies its own profile and reuses this packager as the engine.
13
+ #
14
+ class Profile
15
+
16
+ # @param [String] pod umbrella pod name (e.g. 'Reflex')
17
+ # @param [String] git umbrella pod git repository
18
+ # @param [String] version umbrella pod version (for the tag)
19
+ # @param [Array<String>] libraries ruby lib bundles to add to load path
20
+ # @param [Array<String>] extensions native exts to register (Init_<name>)
21
+ # @param [Array<String>] config_files config file names, preferred first
22
+ # @param [String] template 'new' main script template ({{name}})
23
+ #
24
+ def initialize(pod:, git:, version:, libraries:, extensions:, config_files:, template:)
25
+ @pod, @git, @version, @libraries, @extensions, @config_files, @template =
26
+ pod, git, version, libraries, extensions, config_files, template
27
+ end
28
+
29
+ attr_reader :pod, :git, :version, :libraries, :extensions, :config_files, :template
30
+
31
+ def pod_key()
32
+ pod.downcase.to_sym
33
+ end
34
+
35
+ def bundle_id_prefix()
36
+ "org.xord.#{pod_key}"
37
+ end
38
+
39
+ end# Profile
40
+
41
+
42
+ end# Packager
43
+
44
+
45
+ end# Reflex
@@ -0,0 +1,30 @@
1
+ # -*- mode: ruby -*-
2
+
3
+ platform :macos, '<%= config.macos.deployment_target %>'
4
+
5
+ target '<%= target %>' do
6
+ <%- pod_refs.each do |name, ref| -%>
7
+ <%= pod_line name, ref %>
8
+ <%- end -%>
9
+ end
10
+
11
+ post_install do |installer|
12
+ installer.pods_project.targets.each do |target|
13
+ target.build_configurations.each do |c|
14
+ c.build_settings['ARCHS'] = '<%= config.macos.archs.join ' ' %>'
15
+ c.build_settings['VALID_ARCHS'] = '<%= config.macos.archs.join ' ' %>'
16
+ end
17
+ end
18
+ <%- unless dev_pod_paths.empty? -%>
19
+
20
+ # development pods are not placed under PODS_ROOT, so resolve
21
+ # '${PODS_ROOT}/<name>' to each local directory in the xcconfigs
22
+ Dir.glob('Pods/Target Support Files/**/*.xcconfig').each do |path|
23
+ s = File.read path
24
+ <%- dev_pod_paths.each do |name, dir| -%>
25
+ s.gsub! '${PODS_ROOT}/<%= name %>', '<%= dir %>'
26
+ <%- end -%>
27
+ File.write path, s
28
+ end
29
+ <%- end -%>
30
+ end
@@ -0,0 +1,43 @@
1
+ // -*- mode: objc -*-
2
+ #import <Cocoa/Cocoa.h>
3
+ #import <CRuby.h>
4
+
5
+
6
+ extern "C"
7
+ {
8
+ <%- extensions.each do |ext| -%>
9
+ void Init_<%= ext %> ();
10
+ <%- end -%>
11
+ }
12
+
13
+
14
+ int
15
+ main (int argc, const char* argv[])
16
+ {
17
+ @autoreleasepool
18
+ {
19
+ NSString* appDir =
20
+ [NSBundle.mainBundle.resourcePath stringByAppendingPathComponent:@"app"];
21
+
22
+ // the application loads images by paths relative to the script
23
+ [NSFileManager.defaultManager changeCurrentDirectoryPath:appDir];
24
+
25
+ <%- extensions.each do |ext| -%>
26
+ [CRuby addExtension:@"<%= ext %>" init:^{Init_<%= ext %>();}];
27
+ <%- end -%>
28
+
29
+ for (NSString* lib in @[<%= libraries.map {|l| "@\"#{l}\""}.join ', ' %>])
30
+ [CRuby addLibrary:lib bundle:NSBundle.mainBundle];
31
+
32
+ NSString* main = [appDir stringByAppendingPathComponent:@"<%= config.main %>"];
33
+
34
+ // add the script directory to the load path so that apps split
35
+ // into multiple files can require each other
36
+ [CRuby evaluate:[NSString stringWithFormat:@"$LOAD_PATH.unshift '%@'", appDir]];
37
+
38
+ // the script calls Reflex.start, which runs [NSApp run] and blocks
39
+ // here until the application quits
40
+ [CRuby start:main];
41
+ }
42
+ return 0;
43
+ }
@@ -0,0 +1,40 @@
1
+ name: <%= target %>
2
+
3
+ options:
4
+ deploymentTarget:
5
+ macOS: "<%= config.macos.deployment_target %>"
6
+
7
+ settings:
8
+ base:
9
+ PRODUCT_BUNDLE_IDENTIFIER: <%= config.bundle_id %>
10
+ MARKETING_VERSION: "<%= config.version %>"
11
+ ARCHS: <%= config.macos.archs.join ' ' %>
12
+ ONLY_ACTIVE_ARCH: NO
13
+ CODE_SIGN_IDENTITY: <%= config.macos.codesign_identity.inspect %>
14
+ <%- if config.macos.codesign_team_id -%>
15
+ DEVELOPMENT_TEAM: <%= config.macos.codesign_team_id %>
16
+ <%- end -%>
17
+
18
+ targets:
19
+ <%= target %>:
20
+ type: application
21
+ platform: macOS
22
+ sources:
23
+ - path: src
24
+ - path: app
25
+ type: folder
26
+ buildPhase: resources
27
+ <%- if config.icon -%>
28
+ - path: AppIcon.icns
29
+ buildPhase: resources
30
+ <%- end -%>
31
+ info:
32
+ path: src/Info.plist
33
+ properties:
34
+ CFBundleName: <%= config.name.inspect %>
35
+ CFBundleDisplayName: <%= config.name.inspect %>
36
+ CFBundleShortVersionString: "<%= config.version %>"
37
+ NSHighResolutionCapable: true
38
+ <%- if config.icon -%>
39
+ CFBundleIconFile: AppIcon
40
+ <%- end -%>
@@ -0,0 +1,12 @@
1
+ require 'reflex/packager/extension'
2
+
3
+ require 'reflex/packager/profile'
4
+ require 'reflex/packager/config'
5
+ require 'reflex/packager/platform'
6
+ require 'reflex/packager/macos'
7
+ require 'reflex/packager/cli'
8
+
9
+
10
+ module Reflex::Packager
11
+ PLATFORMS = {macos: MacOS}
12
+ end
@@ -0,0 +1,37 @@
1
+ # -*- mode: ruby -*-
2
+
3
+
4
+ require_relative 'lib/reflex/packager/extension'
5
+
6
+
7
+ Gem::Specification.new do |s|
8
+ glob = -> *patterns do
9
+ patterns.map {|pat| Dir.glob(pat).to_a}.flatten
10
+ end
11
+
12
+ ext = Reflex::Packager::Extension
13
+ name = ext.name true
14
+ rdocs = glob.call *%w[README]
15
+
16
+ s.name = name
17
+ s.version = ext.version
18
+ s.license = 'MIT'
19
+ s.summary = 'Package Reflex applications as native app bundles.'
20
+ s.description = 'CLI tool to package Reflex applications as native macOS application bundles.'
21
+ s.authors = %w[xordog]
22
+ s.email = 'xordog@gmail.com'
23
+ s.homepage = "https://github.com/xord/reflex-packager"
24
+
25
+ s.platform = Gem::Platform::RUBY
26
+ s.required_ruby_version = '>= 3.0.0'
27
+
28
+ s.add_dependency 'xot', '~> 0.3.15'
29
+ s.add_dependency 'rucy', '~> 0.3.15'
30
+ s.add_dependency 'rays', '~> 0.3.16'
31
+ s.add_dependency 'reflexion', '~> 0.5.0'
32
+
33
+ s.files = `git ls-files`.split $/
34
+ s.executables = s.files.grep(%r{^bin/}) {|f| File.basename f}
35
+ s.test_files = s.files.grep %r{^(test|spec|features)/}
36
+ s.extra_rdoc_files = rdocs.to_a
37
+ end