fastlane-plugin-godot 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +122 -0
- data/lib/fastlane/plugin/godot/actions/godot_export_action.rb +119 -0
- data/lib/fastlane/plugin/godot/actions/godot_init_action.rb +89 -0
- data/lib/fastlane/plugin/godot/helper/godot_helper.rb +92 -0
- data/lib/fastlane/plugin/godot/helper/godot_scaffold.rb +181 -0
- data/lib/fastlane/plugin/godot/version.rb +5 -0
- data/lib/fastlane/plugin/godot.rb +15 -0
- metadata +52 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6c9d11e9f58005fe0a36c6cb716b3bc3c9b783b67a268209a06387ca854bc8fa
|
|
4
|
+
data.tar.gz: 0b3982d05a6da3176b43f25bbcbec33335766bddb23c04fd7b438d5b2e718c74
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f97edeb638c693b982100793757a04156ac759a43e0fc594eb62aa637929dacc96177de51ab82f87ae246ae81a9f890a48c2b1e6b394a7f0b922c0ab9004c7d4
|
|
7
|
+
data.tar.gz: 6e8a10c9ecb3258474d84323b597ebb9c4496a7a8c675f8a93fd91c66b445b3055050dea2c864b1f95cdaa34a8bd508be21cf550f1f9a75a46ddea099bbc3944
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mad Science Software
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# fastlane-plugin-godot
|
|
2
|
+
|
|
3
|
+
A [fastlane](https://fastlane.tools) plugin for shipping [Godot Engine](https://godotengine.org)
|
|
4
|
+
games to mobile app stores: headless exports with real diagnostics, composing
|
|
5
|
+
with the fastlane actions (`gym`, `pilot`, `cert`, `sigh`, `supply`, …) that
|
|
6
|
+
already handle signing and store upload.
|
|
7
|
+
|
|
8
|
+
**New to fastlane?** Start with the
|
|
9
|
+
[Godot → TestFlight from-zero walkthrough](examples/ios-testflight/) — a
|
|
10
|
+
complete copy-paste setup including Ruby installation, the App Store Connect
|
|
11
|
+
API key, Godot project prep, and a working Fastfile. First setup ~30 minutes;
|
|
12
|
+
~4 minutes per release after that.
|
|
13
|
+
|
|
14
|
+
**Already using fastlane?**
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
fastlane add_plugin godot
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Actions
|
|
21
|
+
|
|
22
|
+
### godot_init
|
|
23
|
+
|
|
24
|
+
Scaffolds everything a Godot project needs for mobile releases — run once
|
|
25
|
+
from the project directory:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle exec fastlane run godot_init
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Creates (only when missing — existing files are never touched): `Gemfile`,
|
|
32
|
+
`fastlane/{Pluginfile,Appfile,Fastfile,.env.template}`, an
|
|
33
|
+
`export_presets.cfg` with iOS + Android presets, `build/.gdignore` (so Godot
|
|
34
|
+
doesn't import its own build products), a placeholder 1024×1024 `app_icon.png`
|
|
35
|
+
(iOS export hard-fails without an icon; pass `icon:false` to skip), and a
|
|
36
|
+
`.gitignore` (or a warning listing entries yours is missing). Finishes by
|
|
37
|
+
listing the placeholders you must fill in: bundle identifier, team ID, and
|
|
38
|
+
your App Store Connect API key in `fastlane/.env`.
|
|
39
|
+
|
|
40
|
+
### godot_export
|
|
41
|
+
|
|
42
|
+
Exports a Godot project headlessly using a named export preset from
|
|
43
|
+
`export_presets.cfg`, wrapping every footgun we have personally hit shipping
|
|
44
|
+
a real game with it:
|
|
45
|
+
|
|
46
|
+
- verifies the preset exists (and lists the available ones when it doesn't)
|
|
47
|
+
- verifies the matching engine version's export templates are installed for
|
|
48
|
+
the preset's platform, with download guidance when they aren't
|
|
49
|
+
- runs a headless `--import` first — a stale import cache aborts Godot
|
|
50
|
+
exports with a crash-lookalike exit 134
|
|
51
|
+
- creates the output directory (Godot errors rather than create it)
|
|
52
|
+
- confirms the artifact was actually produced — Godot has historically
|
|
53
|
+
exited 0 on some failed exports
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
lane :beta do
|
|
57
|
+
xcodeproj = godot_export(preset: 'iOS')
|
|
58
|
+
build_app(project: xcodeproj, scheme: File.basename(xcodeproj, '.xcodeproj'))
|
|
59
|
+
upload_to_testflight
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Option | Description | Default |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `preset` | Export preset name (required) | — |
|
|
66
|
+
| `project_path` | Directory containing `project.godot` | `.` |
|
|
67
|
+
| `godot_binary` | Path to the Godot binary | `godot` |
|
|
68
|
+
| `output_path` | Artifact destination, relative to the project | the preset's `export_path` |
|
|
69
|
+
| `debug` | Export a debug build | `false` |
|
|
70
|
+
| `import_first` | Run headless `--import` before exporting | `true` |
|
|
71
|
+
| `verbose` | Pass `--verbose` to Godot | `false` |
|
|
72
|
+
|
|
73
|
+
Every option is also settable via environment variable (`FL_GODOT_*`).
|
|
74
|
+
Returns the absolute artifact path (for iOS presets, the generated Xcode
|
|
75
|
+
project) and sets `lane_context[:GODOT_EXPORT_OUTPUT]`.
|
|
76
|
+
|
|
77
|
+
## The iOS signing story (read this once)
|
|
78
|
+
|
|
79
|
+
Godot generates a fresh Xcode project on every export, with two properties
|
|
80
|
+
that break naive pipelines:
|
|
81
|
+
|
|
82
|
+
1. **It writes `CODE_SIGN_IDENTITY="Apple Distribution"` into an
|
|
83
|
+
automatic-signing configuration**
|
|
84
|
+
([godot#110052](https://github.com/godotengine/godot/issues/110052)),
|
|
85
|
+
which xcodebuild rejects as conflicting. Fix: archive with
|
|
86
|
+
`xcargs: 'CODE_SIGN_IDENTITY="Apple Development"'` and let the export
|
|
87
|
+
step re-sign for distribution.
|
|
88
|
+
2. **xcodebuild's cloud signing requires an Admin App Store Connect API
|
|
89
|
+
key** (`Cloud signing permission error` otherwise). You don't need it:
|
|
90
|
+
fastlane's stock `get_certificates` + `get_provisioning_profile` do
|
|
91
|
+
classic signing with an App Manager key, idempotently.
|
|
92
|
+
|
|
93
|
+
The [example Fastfile](examples/ios-testflight/fastlane/Fastfile) wires both
|
|
94
|
+
correctly. Its
|
|
95
|
+
[README's sharp-edges table](examples/ios-testflight/README.md#known-sharp-edges-and-how-this-setup-dodges-them)
|
|
96
|
+
maps the exact error messages to fixes.
|
|
97
|
+
|
|
98
|
+
## Requirements
|
|
99
|
+
|
|
100
|
+
- Godot 4.x available on the `PATH` (or via `godot_binary`)
|
|
101
|
+
- The engine version's export templates installed for your target platform
|
|
102
|
+
- For iOS builds: macOS with Xcode
|
|
103
|
+
|
|
104
|
+
## Roadmap
|
|
105
|
+
|
|
106
|
+
See [ROADMAP.md](ROADMAP.md) — next up: RubyGems release, version/build-number
|
|
107
|
+
management actions, engine-version resolution, Android/Google Play, and
|
|
108
|
+
Continuous Integration (CI) workflow templates.
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
bundle install
|
|
114
|
+
bundle exec rake # runs the RSpec suite
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Issues and pull requests welcome — especially reports of export footguns
|
|
118
|
+
this plugin doesn't yet catch.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
require 'fastlane/action'
|
|
2
|
+
require 'shellwords'
|
|
3
|
+
require_relative '../helper/godot_helper'
|
|
4
|
+
|
|
5
|
+
module Fastlane
|
|
6
|
+
module Actions
|
|
7
|
+
class GodotExportAction < Action
|
|
8
|
+
def self.run(params)
|
|
9
|
+
godot = params[:godot_binary]
|
|
10
|
+
project_path = File.expand_path(params[:project_path])
|
|
11
|
+
preset_name = params[:preset]
|
|
12
|
+
|
|
13
|
+
UI.user_error!("No project.godot in #{project_path}") unless File.exist?(File.join(project_path, 'project.godot'))
|
|
14
|
+
|
|
15
|
+
version = Helper::GodotHelper.godot_version(godot)
|
|
16
|
+
UI.message("Godot #{version}")
|
|
17
|
+
|
|
18
|
+
cfg_path = File.join(project_path, 'export_presets.cfg')
|
|
19
|
+
preset = Helper::GodotHelper.find_preset(cfg_path, preset_name)
|
|
20
|
+
Helper::GodotHelper.verify_templates!(version, preset[:platform])
|
|
21
|
+
|
|
22
|
+
output_path = params[:output_path] || preset[:export_path]
|
|
23
|
+
UI.user_error!("Preset '#{preset_name}' has no export_path and no output_path was given") if output_path.nil? || output_path.empty?
|
|
24
|
+
absolute_output = File.expand_path(output_path, project_path)
|
|
25
|
+
FileUtils.mkdir_p(File.dirname(absolute_output))
|
|
26
|
+
|
|
27
|
+
if params[:import_first]
|
|
28
|
+
UI.message('Importing project resources (stale import caches abort exports)…')
|
|
29
|
+
Actions.sh("#{godot.shellescape} --headless --path #{project_path.shellescape} --import")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
export_flag = params[:debug] ? '--export-debug' : '--export-release'
|
|
33
|
+
command = [
|
|
34
|
+
godot.shellescape,
|
|
35
|
+
'--headless',
|
|
36
|
+
'--path', project_path.shellescape,
|
|
37
|
+
export_flag, preset_name.shellescape,
|
|
38
|
+
absolute_output.shellescape
|
|
39
|
+
]
|
|
40
|
+
command << '--verbose' if params[:verbose]
|
|
41
|
+
|
|
42
|
+
UI.message("Exporting preset '#{preset_name}' (#{preset[:platform]}) -> #{absolute_output}")
|
|
43
|
+
Actions.sh(command.join(' '))
|
|
44
|
+
|
|
45
|
+
# Godot has historically exited 0 on some export failures; trust the artifact, not the exit code.
|
|
46
|
+
UI.user_error!("Export claimed success but produced nothing at #{absolute_output}") unless File.exist?(absolute_output)
|
|
47
|
+
|
|
48
|
+
UI.success("Exported #{preset[:platform]} build: #{absolute_output}")
|
|
49
|
+
Actions.lane_context[SharedValues::GODOT_EXPORT_OUTPUT] = absolute_output
|
|
50
|
+
absolute_output
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.description
|
|
54
|
+
'Export a Godot project headlessly using an export preset'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.return_value
|
|
58
|
+
'Absolute path to the exported artifact (for iOS, the generated Xcode project)'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.authors
|
|
62
|
+
['Mad Science Software']
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.output
|
|
66
|
+
[['GODOT_EXPORT_OUTPUT', 'Absolute path to the exported artifact']]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.available_options
|
|
70
|
+
[
|
|
71
|
+
FastlaneCore::ConfigItem.new(key: :preset,
|
|
72
|
+
env_name: 'FL_GODOT_PRESET',
|
|
73
|
+
description: 'Name of the export preset in export_presets.cfg',
|
|
74
|
+
type: String),
|
|
75
|
+
FastlaneCore::ConfigItem.new(key: :project_path,
|
|
76
|
+
env_name: 'FL_GODOT_PROJECT_PATH',
|
|
77
|
+
description: 'Directory containing project.godot',
|
|
78
|
+
type: String,
|
|
79
|
+
default_value: '.'),
|
|
80
|
+
FastlaneCore::ConfigItem.new(key: :godot_binary,
|
|
81
|
+
env_name: 'FL_GODOT_BINARY',
|
|
82
|
+
description: 'Path to the Godot binary',
|
|
83
|
+
type: String,
|
|
84
|
+
default_value: 'godot'),
|
|
85
|
+
FastlaneCore::ConfigItem.new(key: :output_path,
|
|
86
|
+
env_name: 'FL_GODOT_OUTPUT_PATH',
|
|
87
|
+
description: 'Where to write the exported artifact (defaults to the preset export_path), relative to the project',
|
|
88
|
+
type: String,
|
|
89
|
+
optional: true),
|
|
90
|
+
FastlaneCore::ConfigItem.new(key: :debug,
|
|
91
|
+
env_name: 'FL_GODOT_DEBUG',
|
|
92
|
+
description: 'Export a debug build instead of a release build',
|
|
93
|
+
type: Boolean,
|
|
94
|
+
default_value: false),
|
|
95
|
+
FastlaneCore::ConfigItem.new(key: :import_first,
|
|
96
|
+
env_name: 'FL_GODOT_IMPORT_FIRST',
|
|
97
|
+
description: 'Run a headless --import before exporting (a stale import cache aborts exports)',
|
|
98
|
+
type: Boolean,
|
|
99
|
+
default_value: true),
|
|
100
|
+
FastlaneCore::ConfigItem.new(key: :verbose,
|
|
101
|
+
env_name: 'FL_GODOT_VERBOSE',
|
|
102
|
+
description: 'Pass --verbose to Godot',
|
|
103
|
+
type: Boolean,
|
|
104
|
+
default_value: false)
|
|
105
|
+
]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.is_supported?(platform)
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
module Actions
|
|
115
|
+
module SharedValues
|
|
116
|
+
GODOT_EXPORT_OUTPUT = :GODOT_EXPORT_OUTPUT
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require 'fastlane/action'
|
|
2
|
+
require_relative '../helper/godot_scaffold'
|
|
3
|
+
|
|
4
|
+
module Fastlane
|
|
5
|
+
module Actions
|
|
6
|
+
class GodotInitAction < Action
|
|
7
|
+
def self.run(params)
|
|
8
|
+
project_path = File.expand_path(params[:project_path])
|
|
9
|
+
UI.user_error!("No project.godot in #{project_path} — run this from (or point project_path at) a Godot project") unless File.exist?(File.join(project_path, 'project.godot'))
|
|
10
|
+
|
|
11
|
+
created = []
|
|
12
|
+
skipped = []
|
|
13
|
+
write = lambda do |relative_path, content, binary: false|
|
|
14
|
+
target = File.join(project_path, relative_path)
|
|
15
|
+
if File.exist?(target)
|
|
16
|
+
skipped << relative_path
|
|
17
|
+
else
|
|
18
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
19
|
+
binary ? File.binwrite(target, content) : File.write(target, content)
|
|
20
|
+
created << relative_path
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
scaffold = Helper::GodotScaffold
|
|
25
|
+
write.call('Gemfile', scaffold::GEMFILE)
|
|
26
|
+
write.call('fastlane/Pluginfile', scaffold::PLUGINFILE)
|
|
27
|
+
write.call('fastlane/Appfile', scaffold::APPFILE)
|
|
28
|
+
write.call('fastlane/Fastfile', scaffold::FASTFILE)
|
|
29
|
+
write.call('fastlane/.env.template', scaffold::ENV_TEMPLATE)
|
|
30
|
+
write.call('export_presets.cfg', scaffold::EXPORT_PRESETS)
|
|
31
|
+
write.call('build/.gdignore', '')
|
|
32
|
+
write.call('app_icon.png', scaffold.placeholder_icon_png, binary: true) if params[:icon]
|
|
33
|
+
|
|
34
|
+
gitignore = File.join(project_path, '.gitignore')
|
|
35
|
+
if File.exist?(gitignore)
|
|
36
|
+
existing = File.read(gitignore)
|
|
37
|
+
missing = scaffold::GITIGNORE_ENTRIES.reject { |entry| existing.include?(entry) }
|
|
38
|
+
UI.important("Your .gitignore is missing: #{missing.join(', ')} — add them so credentials and build products stay out of git") unless missing.empty?
|
|
39
|
+
skipped << '.gitignore'
|
|
40
|
+
else
|
|
41
|
+
write.call('.gitignore', scaffold::GITIGNORE)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
created.each { |path| UI.success("created #{path}") }
|
|
45
|
+
skipped.each { |path| UI.message("kept existing #{path}") }
|
|
46
|
+
|
|
47
|
+
unless created.empty?
|
|
48
|
+
UI.important('Placeholders to fill in before building:')
|
|
49
|
+
UI.important(' - fastlane/Appfile: bundle identifier and team ID')
|
|
50
|
+
UI.important(' - export_presets.cfg: bundle identifier, team ID, export_path names')
|
|
51
|
+
UI.important(' - copy fastlane/.env.template to fastlane/.env and add your App Store Connect API key')
|
|
52
|
+
UI.important(' - app_icon.png is an obvious placeholder — replace it before shipping') if params[:icon]
|
|
53
|
+
end
|
|
54
|
+
created
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.description
|
|
58
|
+
'Scaffold the files a Godot project needs for fastlane mobile releases (never overwrites existing files)'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.return_value
|
|
62
|
+
'Array of relative paths that were created'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.authors
|
|
66
|
+
['Mad Science Software']
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.available_options
|
|
70
|
+
[
|
|
71
|
+
FastlaneCore::ConfigItem.new(key: :project_path,
|
|
72
|
+
env_name: 'FL_GODOT_PROJECT_PATH',
|
|
73
|
+
description: 'Directory containing project.godot',
|
|
74
|
+
type: String,
|
|
75
|
+
default_value: '.'),
|
|
76
|
+
FastlaneCore::ConfigItem.new(key: :icon,
|
|
77
|
+
env_name: 'FL_GODOT_INIT_ICON',
|
|
78
|
+
description: 'Also create a placeholder 1024x1024 app_icon.png (iOS export fails without an icon)',
|
|
79
|
+
type: Boolean,
|
|
80
|
+
default_value: true)
|
|
81
|
+
]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.is_supported?(platform)
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require 'fastlane_core/ui/ui'
|
|
2
|
+
|
|
3
|
+
module Fastlane
|
|
4
|
+
UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
|
|
5
|
+
|
|
6
|
+
module Helper
|
|
7
|
+
class GodotHelper
|
|
8
|
+
# Full version string, e.g. "4.7.1.stable.official.a13da4feb"
|
|
9
|
+
def self.godot_version(godot_binary)
|
|
10
|
+
output = `#{godot_binary.shellescape} --version 2>/dev/null`.strip
|
|
11
|
+
UI.user_error!("Could not run '#{godot_binary} --version' — is Godot installed and on the PATH?") if output.empty?
|
|
12
|
+
output.lines.last.strip
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Export templates live in a directory named after the version's
|
|
16
|
+
# release segment: "4.7.1.stable.official.a13da4feb" -> "4.7.1.stable"
|
|
17
|
+
def self.template_directory_name(version_string)
|
|
18
|
+
match = version_string.match(/^(\d+\.\d+(?:\.\d+)?\.[a-z0-9]+)/)
|
|
19
|
+
UI.user_error!("Unrecognized Godot version string: '#{version_string}'") unless match
|
|
20
|
+
match[1]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.templates_root
|
|
24
|
+
if FastlaneCore::Helper.mac?
|
|
25
|
+
File.expand_path('~/Library/Application Support/Godot/export_templates')
|
|
26
|
+
else
|
|
27
|
+
File.expand_path('~/.local/share/godot/export_templates')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Minimal parse of export_presets.cfg: the preset headers plus the
|
|
32
|
+
# name/platform/export_path lines that follow each one.
|
|
33
|
+
def self.parse_presets(cfg_path)
|
|
34
|
+
UI.user_error!("No export_presets.cfg found at #{cfg_path} — create an export preset first (Project > Export in the Godot editor, or commit one)") unless File.exist?(cfg_path)
|
|
35
|
+
|
|
36
|
+
presets = []
|
|
37
|
+
current = nil
|
|
38
|
+
File.foreach(cfg_path) do |line|
|
|
39
|
+
case line
|
|
40
|
+
when /^\[preset\.\d+\]\s*$/
|
|
41
|
+
current = {}
|
|
42
|
+
presets << current
|
|
43
|
+
when /^\[preset\.\d+\.options\]\s*$/
|
|
44
|
+
current = nil
|
|
45
|
+
when /^name="(.*)"\s*$/
|
|
46
|
+
current[:name] = Regexp.last_match(1) if current
|
|
47
|
+
when /^platform="(.*)"\s*$/
|
|
48
|
+
current[:platform] = Regexp.last_match(1) if current
|
|
49
|
+
when /^export_path="(.*)"\s*$/
|
|
50
|
+
current[:export_path] = Regexp.last_match(1) if current
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
presets
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.find_preset(cfg_path, preset_name)
|
|
57
|
+
presets = parse_presets(cfg_path)
|
|
58
|
+
preset = presets.find { |p| p[:name] == preset_name }
|
|
59
|
+
unless preset
|
|
60
|
+
available = presets.map { |p| "'#{p[:name]}' (#{p[:platform]})" }.join(', ')
|
|
61
|
+
UI.user_error!("No export preset named '#{preset_name}' in #{cfg_path}. Available presets: #{available.empty? ? 'none' : available}")
|
|
62
|
+
end
|
|
63
|
+
preset
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The template archive each platform's export requires, keyed by the
|
|
67
|
+
# platform string Godot writes into export_presets.cfg.
|
|
68
|
+
PLATFORM_TEMPLATE_FILES = {
|
|
69
|
+
'iOS' => ['ios.zip'],
|
|
70
|
+
'Android' => ['android_debug.apk', 'android_release.apk', 'android_source.zip'],
|
|
71
|
+
'macOS' => ['macos.zip'],
|
|
72
|
+
'Web' => ['web_release.zip', 'web_debug.zip']
|
|
73
|
+
}.freeze
|
|
74
|
+
|
|
75
|
+
def self.verify_templates!(version_string, platform)
|
|
76
|
+
dir = File.join(templates_root, template_directory_name(version_string))
|
|
77
|
+
needed = PLATFORM_TEMPLATE_FILES[platform]
|
|
78
|
+
return if needed.nil? # platform we don't know how to check — let Godot decide
|
|
79
|
+
|
|
80
|
+
missing = needed.reject { |f| File.exist?(File.join(dir, f)) }
|
|
81
|
+
return if missing.empty?
|
|
82
|
+
|
|
83
|
+
UI.user_error!(
|
|
84
|
+
"Godot #{template_directory_name(version_string)} export templates for #{platform} " \
|
|
85
|
+
"are missing (#{missing.join(', ')} not found in #{dir}). " \
|
|
86
|
+
'Download the templates bundle from https://godotengine.org/download and install it, ' \
|
|
87
|
+
"or unzip the needed files from the .tpz archive into #{dir}"
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
require 'zlib'
|
|
2
|
+
|
|
3
|
+
module Fastlane
|
|
4
|
+
module Helper
|
|
5
|
+
# File templates for godot_init. Placeholder values are ALL-CAPS or
|
|
6
|
+
# com.example so nothing accidentally ships as-is.
|
|
7
|
+
class GodotScaffold
|
|
8
|
+
GEMFILE = <<~RUBY.freeze
|
|
9
|
+
source 'https://rubygems.org'
|
|
10
|
+
|
|
11
|
+
gem 'fastlane'
|
|
12
|
+
|
|
13
|
+
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
|
14
|
+
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
|
15
|
+
RUBY
|
|
16
|
+
|
|
17
|
+
PLUGINFILE = <<~RUBY.freeze
|
|
18
|
+
gem 'fastlane-plugin-godot'
|
|
19
|
+
RUBY
|
|
20
|
+
|
|
21
|
+
APPFILE = <<~RUBY.freeze
|
|
22
|
+
# Your game's bundle identifier (must match the export preset).
|
|
23
|
+
app_identifier('com.example.mygame')
|
|
24
|
+
|
|
25
|
+
# Your 10-character Apple Developer team ID.
|
|
26
|
+
team_id('YOURTEAMID')
|
|
27
|
+
RUBY
|
|
28
|
+
|
|
29
|
+
FASTFILE = <<~RUBY.freeze
|
|
30
|
+
default_platform(:ios)
|
|
31
|
+
|
|
32
|
+
platform :ios do
|
|
33
|
+
desc 'Export from Godot and build a signed .ipa'
|
|
34
|
+
lane :build do
|
|
35
|
+
app_store_connect_api_key(
|
|
36
|
+
key_id: ENV.fetch('ASC_KEY_ID'),
|
|
37
|
+
issuer_id: ENV.fetch('ASC_ISSUER_ID'),
|
|
38
|
+
key_filepath: File.expand_path(ENV.fetch('ASC_KEY_PATH'))
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
xcodeproj = godot_export(preset: 'iOS')
|
|
42
|
+
|
|
43
|
+
# Reuse-or-create the distribution certificate and App Store
|
|
44
|
+
# profile; classic (non-cloud) signing works with an App Manager
|
|
45
|
+
# App Store Connect API key.
|
|
46
|
+
get_certificates
|
|
47
|
+
get_provisioning_profile
|
|
48
|
+
|
|
49
|
+
build_app(
|
|
50
|
+
project: xcodeproj,
|
|
51
|
+
scheme: File.basename(xcodeproj, '.xcodeproj'),
|
|
52
|
+
output_directory: 'build/ios',
|
|
53
|
+
export_method: 'app-store',
|
|
54
|
+
export_options: {
|
|
55
|
+
signingStyle: 'manual',
|
|
56
|
+
signingCertificate: 'Apple Distribution',
|
|
57
|
+
provisioningProfiles: {
|
|
58
|
+
CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) =>
|
|
59
|
+
lane_context[SharedValues::SIGH_NAME]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
# Works around godotengine/godot#110052 (Godot writes a
|
|
63
|
+
# distribution identity into an automatic-signing config).
|
|
64
|
+
xcargs: 'CODE_SIGN_IDENTITY="Apple Development" -allowProvisioningUpdates'
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
desc 'Build and upload to TestFlight'
|
|
69
|
+
lane :beta do
|
|
70
|
+
build
|
|
71
|
+
upload_to_testflight
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
platform :android do
|
|
76
|
+
desc 'Export a debug-signed APK'
|
|
77
|
+
lane :build do
|
|
78
|
+
godot_export(preset: 'Android', debug: true)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
RUBY
|
|
82
|
+
|
|
83
|
+
ENV_TEMPLATE = <<~TEXT.freeze
|
|
84
|
+
# Copy to fastlane/.env and fill in — fastlane loads it automatically.
|
|
85
|
+
# Never commit the real .env; commit only this template.
|
|
86
|
+
ASC_KEY_ID=ABC123DEFG
|
|
87
|
+
ASC_ISSUER_ID=12345678-1234-1234-1234-123456789012
|
|
88
|
+
ASC_KEY_PATH=~/.appstoreconnect/AuthKey_ABC123DEFG.p8
|
|
89
|
+
TEXT
|
|
90
|
+
|
|
91
|
+
GITIGNORE = <<~TEXT.freeze
|
|
92
|
+
# fastlane: local credentials and build products
|
|
93
|
+
fastlane/.env
|
|
94
|
+
fastlane/report.xml
|
|
95
|
+
build/
|
|
96
|
+
vendor/
|
|
97
|
+
.bundle/
|
|
98
|
+
TEXT
|
|
99
|
+
|
|
100
|
+
GITIGNORE_ENTRIES = ['fastlane/.env', 'fastlane/report.xml', 'build/'].freeze
|
|
101
|
+
|
|
102
|
+
EXPORT_PRESETS = <<~TEXT.freeze
|
|
103
|
+
[preset.0]
|
|
104
|
+
|
|
105
|
+
name="iOS"
|
|
106
|
+
platform="iOS"
|
|
107
|
+
runnable=true
|
|
108
|
+
dedicated_server=false
|
|
109
|
+
custom_features=""
|
|
110
|
+
export_filter="all_resources"
|
|
111
|
+
include_filter=""
|
|
112
|
+
exclude_filter=""
|
|
113
|
+
export_path="build/ios/Game.xcodeproj"
|
|
114
|
+
script_export_mode=2
|
|
115
|
+
|
|
116
|
+
[preset.0.options]
|
|
117
|
+
|
|
118
|
+
application/app_store_team_id="YOURTEAMID"
|
|
119
|
+
application/bundle_identifier="com.example.mygame"
|
|
120
|
+
application/short_version="0.1.0"
|
|
121
|
+
application/version="1"
|
|
122
|
+
application/min_ios_version="15.0"
|
|
123
|
+
application/export_project_only=true
|
|
124
|
+
icons/icon_1024x1024="res://app_icon.png"
|
|
125
|
+
orientation/portrait=true
|
|
126
|
+
orientation/upside_down=false
|
|
127
|
+
orientation/landscape_left=false
|
|
128
|
+
orientation/landscape_right=false
|
|
129
|
+
|
|
130
|
+
[preset.1]
|
|
131
|
+
|
|
132
|
+
name="Android"
|
|
133
|
+
platform="Android"
|
|
134
|
+
runnable=true
|
|
135
|
+
dedicated_server=false
|
|
136
|
+
custom_features=""
|
|
137
|
+
export_filter="all_resources"
|
|
138
|
+
include_filter=""
|
|
139
|
+
exclude_filter=""
|
|
140
|
+
export_path="build/android/Game.apk"
|
|
141
|
+
script_export_mode=2
|
|
142
|
+
|
|
143
|
+
[preset.1.options]
|
|
144
|
+
|
|
145
|
+
gradle_build/use_gradle_build=false
|
|
146
|
+
architectures/arm64-v8a=true
|
|
147
|
+
version/code=1
|
|
148
|
+
version/name="0.1.0"
|
|
149
|
+
package/unique_name="com.example.mygame"
|
|
150
|
+
package/name=""
|
|
151
|
+
package/signed=true
|
|
152
|
+
launcher_icons/main_192x192="res://app_icon.png"
|
|
153
|
+
screen/immersive_mode=true
|
|
154
|
+
TEXT
|
|
155
|
+
|
|
156
|
+
# Minimal valid 1024x1024 opaque PNG (Apple rejects alpha in the
|
|
157
|
+
# marketing icon): flat two-tone placeholder, obviously temporary.
|
|
158
|
+
def self.placeholder_icon_png(size = 1024)
|
|
159
|
+
background = [40, 90, 100].pack('C3')
|
|
160
|
+
foreground = [235, 180, 70].pack('C3')
|
|
161
|
+
low = size / 4
|
|
162
|
+
high = 3 * size / 4
|
|
163
|
+
raw = +''.b
|
|
164
|
+
size.times do |y|
|
|
165
|
+
raw << "\x00".b
|
|
166
|
+
size.times do |x|
|
|
167
|
+
raw << (x >= low && x < high && y >= low && y < high ? foreground : background)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
chunk = lambda do |tag, data|
|
|
171
|
+
[data.bytesize].pack('N') + tag + data + [Zlib.crc32(tag + data)].pack('N')
|
|
172
|
+
end
|
|
173
|
+
header = [size, size, 8, 2, 0, 0, 0].pack('NNC5')
|
|
174
|
+
"\x89PNG\r\n\x1a\n".b +
|
|
175
|
+
chunk.call('IHDR'.b, header) +
|
|
176
|
+
chunk.call('IDAT'.b, Zlib::Deflate.deflate(raw, Zlib::BEST_COMPRESSION)) +
|
|
177
|
+
chunk.call('IEND'.b, ''.b)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require 'fastlane/plugin/godot/version'
|
|
2
|
+
|
|
3
|
+
module Fastlane
|
|
4
|
+
module Godot
|
|
5
|
+
# Return all .rb files inside the "actions" and "helper" directories
|
|
6
|
+
def self.all_classes
|
|
7
|
+
Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# By default, fastlane loads all actions and helpers of a plugin at once
|
|
13
|
+
Fastlane::Godot.all_classes.each do |current|
|
|
14
|
+
require current
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fastlane-plugin-godot
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mad Science Software
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: 'Godot-specific fastlane actions: headless engine exports, export template
|
|
13
|
+
verification, and Xcode project handling for shipping Godot games to the app stores.'
|
|
14
|
+
email:
|
|
15
|
+
- jamesyoungwrites@gmail.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/fastlane/plugin/godot.rb
|
|
23
|
+
- lib/fastlane/plugin/godot/actions/godot_export_action.rb
|
|
24
|
+
- lib/fastlane/plugin/godot/actions/godot_init_action.rb
|
|
25
|
+
- lib/fastlane/plugin/godot/helper/godot_helper.rb
|
|
26
|
+
- lib/fastlane/plugin/godot/helper/godot_scaffold.rb
|
|
27
|
+
- lib/fastlane/plugin/godot/version.rb
|
|
28
|
+
homepage: https://github.com/Mad-Science-Software/fastlane-plugin-godot
|
|
29
|
+
licenses:
|
|
30
|
+
- MIT
|
|
31
|
+
metadata:
|
|
32
|
+
rubygems_mfa_required: 'true'
|
|
33
|
+
source_code_uri: https://github.com/Mad-Science-Software/fastlane-plugin-godot
|
|
34
|
+
changelog_uri: https://github.com/Mad-Science-Software/fastlane-plugin-godot/releases
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
require_paths:
|
|
37
|
+
- lib
|
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '3.1'
|
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
requirements: []
|
|
49
|
+
rubygems_version: 4.0.16
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: Export and ship Godot Engine games with fastlane
|
|
52
|
+
test_files: []
|