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.
@@ -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