DYXCFrameworkBuilder 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/.idea/.gitignore +8 -0
- data/.idea/DYXCFrameworkBuilder.iml +72 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/ARCHITECTURE.md +163 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +143 -0
- data/README.md +338 -0
- data/Rakefile +4 -0
- data/bin/xcbuilder +230 -0
- data/lib/DYXCFrameworkBuilder/builder.rb +136 -0
- data/lib/DYXCFrameworkBuilder/config.rb +122 -0
- data/lib/DYXCFrameworkBuilder/framework_podspec_generator.rb +157 -0
- data/lib/DYXCFrameworkBuilder/framework_publisher.rb +131 -0
- data/lib/DYXCFrameworkBuilder/oss_uploader.rb +210 -0
- data/lib/DYXCFrameworkBuilder/podspec_parser.rb +113 -0
- data/lib/DYXCFrameworkBuilder/version.rb +5 -0
- data/lib/DYXCFrameworkBuilder/xcframework_builder.rb +483 -0
- data/lib/DYXCFrameworkBuilder/yaml_generator.rb +292 -0
- data/lib/DYXCFrameworkBuilder.rb +45 -0
- data/sig/DYXCFrameworkBuilder.rbs +4 -0
- metadata +114 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module DYXCFrameworkBuilder
|
6
|
+
class Config
|
7
|
+
attr_reader :project_path, :scheme, :target, :configuration, :output_dir,
|
8
|
+
:platforms, :deployment_target, :enable_bitcode, :skip_warnings
|
9
|
+
|
10
|
+
def initialize(yaml_path)
|
11
|
+
@yaml_path = yaml_path
|
12
|
+
load_config
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.create_template(path)
|
16
|
+
template = {
|
17
|
+
'project' => {
|
18
|
+
'path' => './MyLibrary.xcworkspace',
|
19
|
+
'scheme' => 'MyLibrary',
|
20
|
+
'target' => 'MyLibrary', # Optional: specific target to build (if different from scheme)
|
21
|
+
'configuration' => 'Release'
|
22
|
+
},
|
23
|
+
'output' => {
|
24
|
+
'directory' => './build',
|
25
|
+
'framework_name' => 'MyLibrary.xcframework'
|
26
|
+
},
|
27
|
+
'platforms' => {
|
28
|
+
'ios' => {
|
29
|
+
'deployment_target' => '11.0',
|
30
|
+
'architectures' => ['arm64']
|
31
|
+
}
|
32
|
+
},
|
33
|
+
'build_settings' => {
|
34
|
+
'enable_bitcode' => false,
|
35
|
+
'skip_warnings' => true,
|
36
|
+
'swift_version' => '5.0'
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
File.write(path, template.to_yaml)
|
41
|
+
puts "Created configuration template at: #{path}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def framework_name
|
45
|
+
@framework_name || "#{@scheme}.xcframework"
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def load_config
|
51
|
+
unless File.exist?(@yaml_path)
|
52
|
+
raise Error, "Configuration file not found: #{@yaml_path}"
|
53
|
+
end
|
54
|
+
|
55
|
+
begin
|
56
|
+
config = YAML.load_file(@yaml_path)
|
57
|
+
parse_config(config)
|
58
|
+
rescue Psych::SyntaxError => e
|
59
|
+
raise DYXCFrameworkBuilder::Error, "Invalid YAML syntax in #{@yaml_path}: #{e.message}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def parse_config(config)
|
64
|
+
project_config = config['project'] || {}
|
65
|
+
output_config = config['output'] || {}
|
66
|
+
build_config = config['build_settings'] || {}
|
67
|
+
|
68
|
+
@project_path = project_config['path']
|
69
|
+
@scheme = project_config['scheme']
|
70
|
+
@target = project_config['target']
|
71
|
+
@configuration = project_config['configuration'] || 'Release'
|
72
|
+
|
73
|
+
@output_dir = output_config['directory'] || './build'
|
74
|
+
@framework_name = output_config['framework_name']
|
75
|
+
|
76
|
+
@platforms = config['platforms'] || {}
|
77
|
+
@enable_bitcode = build_config['enable_bitcode'] || false
|
78
|
+
@skip_warnings = build_config['skip_warnings'] || true
|
79
|
+
@swift_version = build_config['swift_version'] || '5.0'
|
80
|
+
|
81
|
+
validate_config
|
82
|
+
end
|
83
|
+
|
84
|
+
def validate_config
|
85
|
+
required_fields = [@project_path, @scheme]
|
86
|
+
missing_fields = []
|
87
|
+
|
88
|
+
missing_fields << 'project.path' if @project_path.nil? || @project_path.empty?
|
89
|
+
missing_fields << 'project.scheme' if @scheme.nil? || @scheme.empty?
|
90
|
+
|
91
|
+
unless missing_fields.empty?
|
92
|
+
raise DYXCFrameworkBuilder::Error, "Missing required configuration fields: #{missing_fields.join(', ')}"
|
93
|
+
end
|
94
|
+
|
95
|
+
# Check if project file exists (support both .xcworkspace and .xcodeproj)
|
96
|
+
unless validate_project_file(@project_path)
|
97
|
+
raise DYXCFrameworkBuilder::Error, "Project file not found: #{@project_path}"
|
98
|
+
end
|
99
|
+
|
100
|
+
# Log target usage
|
101
|
+
if @target && @target != @scheme
|
102
|
+
puts "[INFO] Using specific target: #{@target} (different from scheme: #{@scheme})"
|
103
|
+
elsif @target.nil? || @target.empty?
|
104
|
+
puts "[INFO] No specific target specified, using scheme: #{@scheme}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def validate_project_file(project_path)
|
111
|
+
# Direct file existence check
|
112
|
+
return true if File.exist?(project_path)
|
113
|
+
|
114
|
+
# If no extension provided, try both .xcworkspace and .xcodeproj
|
115
|
+
if File.extname(project_path).empty?
|
116
|
+
return File.exist?("#{project_path}.xcworkspace") || File.exist?("#{project_path}.xcodeproj")
|
117
|
+
end
|
118
|
+
|
119
|
+
false
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module DYXCFrameworkBuilder
|
6
|
+
class FrameworkPodspecGenerator
|
7
|
+
attr_reader :original_parser, :xcframework_path, :base_url
|
8
|
+
|
9
|
+
def initialize(original_parser, xcframework_path, base_url = nil, use_oss = false, oss_config = {})
|
10
|
+
@original_parser = original_parser
|
11
|
+
@xcframework_path = xcframework_path
|
12
|
+
@base_url = base_url || 'https://git.diyiedu.com/components/xcframework_bundle/-/blob/master'
|
13
|
+
@use_oss = use_oss
|
14
|
+
@oss_config = oss_config
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate_framework_podspec(version_suffix = '-xc')
|
18
|
+
framework_name = @original_parser.name # podspec 内容中的 name 使用原始名称
|
19
|
+
version_xc = "#{@original_parser.version}#{version_suffix}"
|
20
|
+
|
21
|
+
# 构建下载 URL
|
22
|
+
download_url = if @use_oss
|
23
|
+
upload_to_oss_and_get_url(version_xc)
|
24
|
+
else
|
25
|
+
build_download_url(version_xc)
|
26
|
+
end
|
27
|
+
|
28
|
+
# 生成 podspec 内容
|
29
|
+
podspec_content = build_podspec_content(framework_name, version_xc, download_url)
|
30
|
+
|
31
|
+
# 创建 build 目录
|
32
|
+
build_dir = File.join(@original_parser.podspec_dir, 'build')
|
33
|
+
FileUtils.mkdir_p(build_dir)
|
34
|
+
|
35
|
+
# 写入文件 (文件名与原 podspec 保持一致)
|
36
|
+
original_podspec_name = File.basename(@original_parser.podspec_path, '.podspec')
|
37
|
+
output_path = File.join(build_dir, "#{original_podspec_name}.podspec")
|
38
|
+
File.write(output_path, podspec_content)
|
39
|
+
|
40
|
+
puts "✅ Framework podspec generated: #{output_path}"
|
41
|
+
puts "📁 Output directory: ./build/"
|
42
|
+
puts "📦 Framework name: #{framework_name}"
|
43
|
+
puts "🔢 Version: #{version_xc}"
|
44
|
+
puts "🌐 Download URL: #{download_url}"
|
45
|
+
|
46
|
+
{
|
47
|
+
path: output_path,
|
48
|
+
name: framework_name,
|
49
|
+
version: version_xc,
|
50
|
+
download_url: download_url
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def build_download_url(version_xc)
|
57
|
+
framework_filename = "#{@original_parser.name}.xcframework.zip"
|
58
|
+
"@#{@base_url}/#{@original_parser.name}/#{version_xc}/#{framework_filename}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_podspec_content(framework_name, version_xc, download_url)
|
62
|
+
# 获取原始 podspec 的内容用于复用
|
63
|
+
deployment_target = @original_parser.deployment_target || '11.0'
|
64
|
+
|
65
|
+
<<~PODSPEC
|
66
|
+
Pod::Spec.new do |s|
|
67
|
+
s.name = '#{framework_name}'
|
68
|
+
s.version = '#{version_xc}'
|
69
|
+
s.summary = '#{get_summary}'
|
70
|
+
s.description = <<-DESC
|
71
|
+
#{get_description}
|
72
|
+
DESC
|
73
|
+
|
74
|
+
s.homepage = '#{get_homepage}'
|
75
|
+
s.license = #{get_license}
|
76
|
+
s.author = #{get_author}
|
77
|
+
s.source = { :http => '#{download_url.sub('@', '')}' }
|
78
|
+
|
79
|
+
s.ios.deployment_target = '#{deployment_target}'
|
80
|
+
|
81
|
+
s.vendored_frameworks = '#{framework_name}.xcframework'
|
82
|
+
|
83
|
+
# 资源文件(如果有的话)
|
84
|
+
#{get_resource_bundles}
|
85
|
+
|
86
|
+
# 依赖项(如果需要的话)
|
87
|
+
#{get_dependencies}
|
88
|
+
|
89
|
+
# 框架依赖
|
90
|
+
#{get_frameworks}
|
91
|
+
end
|
92
|
+
PODSPEC
|
93
|
+
end
|
94
|
+
|
95
|
+
def get_summary
|
96
|
+
"XCFramework version of #{@original_parser.name}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def get_description
|
100
|
+
"Pre-compiled XCFramework for #{@original_parser.name}. This framework includes support for both iOS devices and simulators."
|
101
|
+
end
|
102
|
+
|
103
|
+
def get_homepage
|
104
|
+
"https://git.diyiedu.com/components/#{@original_parser.name}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def get_license
|
108
|
+
"{ :type => 'MIT', :file => 'LICENSE' }"
|
109
|
+
end
|
110
|
+
|
111
|
+
def get_author
|
112
|
+
"{ 'DY Components' => 'components@diyiedu.com' }"
|
113
|
+
end
|
114
|
+
|
115
|
+
def get_resource_bundles
|
116
|
+
"# s.resource_bundles = {\n # '#{@original_parser.name}' => ['#{@original_parser.name}/Assets/*.png']\n # }"
|
117
|
+
end
|
118
|
+
|
119
|
+
def get_dependencies
|
120
|
+
"# s.dependency 'SomeFramework', '~> 1.0'"
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_frameworks
|
124
|
+
"# s.frameworks = 'UIKit', 'Foundation'"
|
125
|
+
end
|
126
|
+
|
127
|
+
def upload_to_oss_and_get_url(version_xc)
|
128
|
+
puts ""
|
129
|
+
puts "🌐 Uploading XCFramework to Alibaba Cloud OSS..."
|
130
|
+
|
131
|
+
# 上传到 OSS - 使用原始 podspec 的 version,不是带 _xc 后缀的版本
|
132
|
+
original_version = @original_parser.version
|
133
|
+
upload_result = OSSUploader.upload_framework(
|
134
|
+
@xcframework_path,
|
135
|
+
@original_parser.name,
|
136
|
+
original_version, # 使用原始 version
|
137
|
+
@oss_config
|
138
|
+
)
|
139
|
+
|
140
|
+
if upload_result[:success]
|
141
|
+
puts " ✅ OSS upload successful"
|
142
|
+
# 返回带 @ 前缀的下载地址
|
143
|
+
"@#{upload_result[:download_url]}"
|
144
|
+
else
|
145
|
+
puts " ❌ OSS upload failed: #{upload_result[:error]}"
|
146
|
+
puts " 🔄 Falling back to GitLab URL format"
|
147
|
+
# 失败时回退到 GitLab 格式
|
148
|
+
build_download_url(version_xc)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.generate_from_original(original_parser, xcframework_path, base_url = nil, version_suffix = '-xc', use_oss = false, oss_config = {})
|
153
|
+
generator = new(original_parser, xcframework_path, base_url, use_oss, oss_config)
|
154
|
+
generator.generate_framework_podspec(version_suffix)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
module DYXCFrameworkBuilder
|
6
|
+
class FrameworkPublisher
|
7
|
+
attr_reader :podspec_path, :repo_name, :sources, :options
|
8
|
+
|
9
|
+
DEFAULT_REPO = 'ios-specs'
|
10
|
+
DEFAULT_SOURCES = 'http://git.diyiedu.com/yangtianyan/ios-specs.git,https://github.com/CocoaPods/Specs'
|
11
|
+
|
12
|
+
def initialize(podspec_path, repo_name = nil, sources = nil)
|
13
|
+
@podspec_path = podspec_path
|
14
|
+
@repo_name = repo_name || DEFAULT_REPO
|
15
|
+
@sources = sources || DEFAULT_SOURCES
|
16
|
+
@options = {
|
17
|
+
skip_import_validation: true,
|
18
|
+
allow_warnings: true,
|
19
|
+
verbose: true
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def publish
|
24
|
+
puts "🚀 Starting framework publishing process..."
|
25
|
+
puts "📄 Podspec: #{@podspec_path}"
|
26
|
+
puts "📚 Repository: #{@repo_name}"
|
27
|
+
puts "🔗 Sources: #{@sources}"
|
28
|
+
puts "="*60
|
29
|
+
|
30
|
+
# 验证 podspec 文件存在
|
31
|
+
unless File.exist?(@podspec_path)
|
32
|
+
raise DYXCFrameworkBuilder::Error, "Podspec file not found: #{@podspec_path}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# 构建并执行发布命令
|
36
|
+
command = build_publish_command
|
37
|
+
puts "🔧 Executing command:"
|
38
|
+
puts " #{command}"
|
39
|
+
puts ""
|
40
|
+
|
41
|
+
success = execute_command(command)
|
42
|
+
|
43
|
+
if success
|
44
|
+
puts "="*60
|
45
|
+
puts "✅ Framework published successfully!"
|
46
|
+
puts "🎉 Your XCFramework is now available in the private repository!"
|
47
|
+
else
|
48
|
+
puts "="*60
|
49
|
+
puts "❌ Publishing failed. Please check the error messages above."
|
50
|
+
puts "💡 You can try running the command manually:"
|
51
|
+
puts " #{command}"
|
52
|
+
end
|
53
|
+
|
54
|
+
success
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.publish_framework(podspec_path, repo_name = nil, sources = nil)
|
58
|
+
publisher = new(podspec_path, repo_name, sources)
|
59
|
+
publisher.publish
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def build_publish_command
|
65
|
+
cmd_parts = [
|
66
|
+
'pod repo push',
|
67
|
+
@repo_name,
|
68
|
+
@podspec_path,
|
69
|
+
"--sources='#{@sources}'"
|
70
|
+
]
|
71
|
+
|
72
|
+
# 添加选项
|
73
|
+
cmd_parts << '--skip-import-validation' if @options[:skip_import_validation]
|
74
|
+
cmd_parts << '--allow-warnings' if @options[:allow_warnings]
|
75
|
+
cmd_parts << '--verbose' if @options[:verbose]
|
76
|
+
|
77
|
+
cmd_parts.join(' ')
|
78
|
+
end
|
79
|
+
|
80
|
+
def execute_command(command)
|
81
|
+
puts "⏳ Publishing to repository..."
|
82
|
+
|
83
|
+
# 使用 Open3 来执行命令并实时显示输出
|
84
|
+
success = false
|
85
|
+
|
86
|
+
Open3.popen2e(command) do |stdin, stdout_stderr, wait_thread|
|
87
|
+
stdout_stderr.each_line do |line|
|
88
|
+
puts " #{line}"
|
89
|
+
end
|
90
|
+
|
91
|
+
success = wait_thread.value.success?
|
92
|
+
end
|
93
|
+
|
94
|
+
success
|
95
|
+
rescue => e
|
96
|
+
puts "❌ Command execution failed: #{e.message}"
|
97
|
+
false
|
98
|
+
end
|
99
|
+
|
100
|
+
# 验证环境和依赖
|
101
|
+
def validate_environment
|
102
|
+
# 检查是否安装了 CocoaPods
|
103
|
+
unless system('which pod > /dev/null 2>&1')
|
104
|
+
raise DYXCFrameworkBuilder::Error, "CocoaPods is not installed. Please install it first: gem install cocoapods"
|
105
|
+
end
|
106
|
+
|
107
|
+
# 检查是否配置了指定的 repo
|
108
|
+
repo_list = `pod repo list`.strip
|
109
|
+
unless repo_list.include?(@repo_name)
|
110
|
+
puts "⚠️ Repository '#{@repo_name}' not found. You may need to add it first:"
|
111
|
+
puts " pod repo add #{@repo_name} <repo_url>"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# 预发布验证
|
116
|
+
def pre_publish_validation
|
117
|
+
puts "🔍 Validating podspec before publishing..."
|
118
|
+
|
119
|
+
validation_command = "pod spec lint #{@podspec_path} --sources='#{@sources}' --allow-warnings"
|
120
|
+
puts " #{validation_command}"
|
121
|
+
|
122
|
+
validation_success = system(validation_command)
|
123
|
+
|
124
|
+
unless validation_success
|
125
|
+
puts "⚠️ Podspec validation failed. Publishing anyway due to --allow-warnings flag."
|
126
|
+
end
|
127
|
+
|
128
|
+
validation_success
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zip'
|
4
|
+
require 'aliyun/oss'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module DYXCFrameworkBuilder
|
8
|
+
class OSSUploader
|
9
|
+
attr_reader :access_key_id, :access_key_secret, :bucket_name, :endpoint, :region
|
10
|
+
|
11
|
+
def initialize(config = {})
|
12
|
+
@access_key_id = config[:access_key_id] || ENV['ALIBABA_CLOUD_ACCESS_KEY_ID'] || "LTAI5t9QpNarX4C5dmzx3zst"
|
13
|
+
@access_key_secret = config[:access_key_secret] || ENV['ALIBABA_CLOUD_ACCESS_KEY_SECRET'] || "EdbGxVVkxHVYmCUVjaDMMVs5b8Rsa9"
|
14
|
+
@bucket_name = config[:bucket] || ENV['OSS_BUCKET'] || "diyi-oss-f"
|
15
|
+
@endpoint = config[:endpoint] || ENV['OSS_ENDPOINT'] || "oss-cn-hangzhou.aliyuncs.com"
|
16
|
+
@region = config[:region] || ENV['OSS_REGION'] || ''
|
17
|
+
|
18
|
+
# 不进行提前校验,在实际使用时再校验
|
19
|
+
initialize_client if should_initialize_client?
|
20
|
+
end
|
21
|
+
|
22
|
+
def upload_xcframework(xcframework_path, framework_name, version)
|
23
|
+
puts "🚀 Starting XCFramework upload to Alibaba Cloud OSS..."
|
24
|
+
puts "📦 Framework: #{framework_name}"
|
25
|
+
puts "🔢 Version: #{version}"
|
26
|
+
puts "📁 Source: #{xcframework_path}"
|
27
|
+
puts "="*60
|
28
|
+
|
29
|
+
# 在使用时进行配置校验
|
30
|
+
validate_config_on_use
|
31
|
+
|
32
|
+
# 如果客户端未初始化,现在初始化
|
33
|
+
initialize_client unless @client
|
34
|
+
|
35
|
+
# 1. 验证 XCFramework 文件存在
|
36
|
+
unless File.exist?(xcframework_path)
|
37
|
+
raise DYXCFrameworkBuilder::Error, "XCFramework not found: #{xcframework_path}"
|
38
|
+
end
|
39
|
+
|
40
|
+
# 2. 创建压缩包
|
41
|
+
zip_path = create_zip_package(xcframework_path, framework_name)
|
42
|
+
file_size = File.size(zip_path)
|
43
|
+
|
44
|
+
# 3. 生成 OSS 上传路径
|
45
|
+
oss_path = generate_bucket_path(framework_name, version)
|
46
|
+
|
47
|
+
# 4. 上传到 OSS
|
48
|
+
download_url = upload_to_oss(zip_path, oss_path)
|
49
|
+
|
50
|
+
# 5. 清理临时文件
|
51
|
+
FileUtils.rm_f(zip_path)
|
52
|
+
|
53
|
+
puts "="*60
|
54
|
+
puts "✅ XCFramework uploaded successfully!"
|
55
|
+
puts "🌐 Download URL: #{download_url}"
|
56
|
+
|
57
|
+
{
|
58
|
+
success: true,
|
59
|
+
download_url: download_url,
|
60
|
+
oss_path: oss_path,
|
61
|
+
file_size: file_size
|
62
|
+
}
|
63
|
+
rescue => e
|
64
|
+
# 清理临时文件
|
65
|
+
FileUtils.rm_f(zip_path) if zip_path && File.exist?(zip_path)
|
66
|
+
|
67
|
+
puts "="*60
|
68
|
+
puts "❌ Upload failed: #{e.message}"
|
69
|
+
|
70
|
+
{
|
71
|
+
success: false,
|
72
|
+
error: e.message
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.upload_framework(xcframework_path, framework_name, version, config = {})
|
77
|
+
uploader = new(config)
|
78
|
+
uploader.upload_xcframework(xcframework_path, framework_name, version)
|
79
|
+
end
|
80
|
+
|
81
|
+
# 生成 OSS 对象路径 (bucket_name)
|
82
|
+
def generate_bucket_path(framework_name, version)
|
83
|
+
now = Time.now
|
84
|
+
year = now.strftime('%Y')
|
85
|
+
month = now.strftime('%m')
|
86
|
+
day = now.strftime('%d')
|
87
|
+
|
88
|
+
"iOS/frameworks/#{year}/#{month}/#{day}/#{version}/#{framework_name}.xcframework.zip"
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def should_initialize_client?
|
94
|
+
# 只有当所有必需配置都存在时才初始化客户端
|
95
|
+
![@access_key_id, @access_key_secret, @bucket_name, @endpoint].any? { |config|
|
96
|
+
config.nil? || config.empty? || config == "your_access_key_id" ||
|
97
|
+
config == "your_access_key_secret" || config == "your_bucket_name"
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate_config_on_use
|
102
|
+
required_configs = {
|
103
|
+
access_key_id: @access_key_id,
|
104
|
+
access_key_secret: @access_key_secret,
|
105
|
+
bucket_name: @bucket_name,
|
106
|
+
endpoint: @endpoint
|
107
|
+
}
|
108
|
+
|
109
|
+
missing_configs = required_configs.select { |key, value|
|
110
|
+
value.nil? || value.empty? || value.start_with?("your_")
|
111
|
+
}
|
112
|
+
|
113
|
+
unless missing_configs.empty?
|
114
|
+
missing_keys = missing_configs.keys.join(', ')
|
115
|
+
raise DYXCFrameworkBuilder::Error,
|
116
|
+
"Missing OSS configuration: #{missing_keys}. " \
|
117
|
+
"Please set environment variables or pass them in config."
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def initialize_client
|
122
|
+
return if @client # 避免重复初始化
|
123
|
+
|
124
|
+
# 确保 endpoint 包含 https://
|
125
|
+
endpoint_url = @endpoint.start_with?('http') ? @endpoint : "https://#{@endpoint}"
|
126
|
+
|
127
|
+
# 初始化阿里云 OSS 客户端
|
128
|
+
@client = Aliyun::OSS::Client.new(
|
129
|
+
endpoint: endpoint_url,
|
130
|
+
access_key_id: @access_key_id,
|
131
|
+
access_key_secret: @access_key_secret
|
132
|
+
)
|
133
|
+
|
134
|
+
# 获取 bucket 实例
|
135
|
+
@bucket = @client.get_bucket(@bucket_name)
|
136
|
+
|
137
|
+
puts "✅ OSS client initialized successfully"
|
138
|
+
puts " 📍 Endpoint: #{endpoint_url}"
|
139
|
+
puts " 🗂️ Bucket: #{@bucket_name}"
|
140
|
+
|
141
|
+
rescue => e
|
142
|
+
raise DYXCFrameworkBuilder::Error, "Failed to initialize OSS client: #{e.message}"
|
143
|
+
end
|
144
|
+
|
145
|
+
def create_zip_package(xcframework_path, framework_name)
|
146
|
+
zip_filename = "#{framework_name}.xcframework.zip"
|
147
|
+
zip_path = File.join(File.dirname(xcframework_path), zip_filename)
|
148
|
+
|
149
|
+
puts "📦 Creating zip package: #{zip_filename}"
|
150
|
+
|
151
|
+
# 删除已存在的 zip 文件
|
152
|
+
FileUtils.rm_f(zip_path)
|
153
|
+
|
154
|
+
# 创建 zip 文件
|
155
|
+
Zip::File.open(zip_path, Zip::File::CREATE) do |zipfile|
|
156
|
+
# 递归添加 XCFramework 目录中的所有文件
|
157
|
+
add_directory_to_zip(zipfile, xcframework_path, File.basename(xcframework_path))
|
158
|
+
end
|
159
|
+
|
160
|
+
puts " ✅ Zip created: #{File.basename(zip_path)} (#{format_file_size(File.size(zip_path))})"
|
161
|
+
zip_path
|
162
|
+
end
|
163
|
+
|
164
|
+
def add_directory_to_zip(zipfile, source_dir, zip_dir_name)
|
165
|
+
Dir.glob(File.join(source_dir, '**', '*')).each do |file_path|
|
166
|
+
# 计算相对路径
|
167
|
+
relative_path = file_path.sub(source_dir, zip_dir_name)
|
168
|
+
|
169
|
+
if File.directory?(file_path)
|
170
|
+
zipfile.mkdir(relative_path) unless zipfile.find_entry(relative_path)
|
171
|
+
else
|
172
|
+
zipfile.add(relative_path, file_path)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
|
179
|
+
def upload_to_oss(local_file_path, oss_path)
|
180
|
+
puts "☁️ Uploading to OSS: #{oss_path}"
|
181
|
+
|
182
|
+
# 使用阿里云 SDK 上传文件
|
183
|
+
@bucket.put_object(oss_path, file: local_file_path)
|
184
|
+
|
185
|
+
puts " ✅ Upload successful"
|
186
|
+
|
187
|
+
# 生成下载链接
|
188
|
+
generate_download_url(oss_path)
|
189
|
+
rescue => e
|
190
|
+
raise "OSS upload failed: #{e.message}"
|
191
|
+
end
|
192
|
+
|
193
|
+
def generate_download_url(oss_path)
|
194
|
+
# 使用固定地址生成公共访问的下载 URL
|
195
|
+
"https://f.diyiedu.com/#{oss_path}"
|
196
|
+
end
|
197
|
+
|
198
|
+
def format_file_size(size)
|
199
|
+
units = ['B', 'KB', 'MB', 'GB']
|
200
|
+
unit_index = 0
|
201
|
+
|
202
|
+
while size >= 1024 && unit_index < units.length - 1
|
203
|
+
size /= 1024.0
|
204
|
+
unit_index += 1
|
205
|
+
end
|
206
|
+
|
207
|
+
"#{size.round(2)} #{units[unit_index]}"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|