require 'cocoapods-flutter/native/archive/command_runner' require 'cocoapods' require 'zip' require 'cocoapods-flutter/native/archive/zip_tool' require 'cocoapods-flutter/native/archive/remote' require 'molinillo' require 'plist' require 'yaml' class Archiver class Plugins attr_reader :spec attr_reader :path attr_reader :name def initialize(name, path, spec) @name = name @path = path @spec = spec end end class Dependency < Hash attr_reader :name attr_reader :version def initialize(name, version) @name = name @version = version end def to_hash() {:name => name, :version => version} end end public def initialize(module_name, version, sources, flutter_wrapper, pub_upgrade, flutter_version, build_run, working_dir, repo, build_modes) @module_name = module_name @version = version @sources = sources @flutter_wrapper = flutter_wrapper @pub_upgrade = pub_upgrade @flutter_version = flutter_version @build_run = build_run @working_dir = working_dir @product_dir = "#{@working_dir}/.product/" @pod_repo = repo @build_modes = build_modes end def archive # open('http://127.0.0.1:8080/frameworks/download/Flutter/1.22.6/release/file.zip') do |u| # File.open('/Users/dreamtracer/Desktop/flutter.zip', "w+") do |file| # file.write u.read.force_encoding("UTF-8") # end # end #使用fvm切换flutter 版本 flutter_sdk_version_switch @flutter_version @pub_upgrade ? pub_upgrade : pub_get if local_podfile_exists? use_local_podfile else setup_pod_sources end if @build_run excute_build_run end build_framework @plugins = fetch_plugins if @build_modes.include?('debug') # Debug瘦身 #thin_arm64 "#{@product_dir}Debug/App.framework/App" debug end if @build_modes.include?('release') release end Pod::UserInterface.message "All is ready to use!, now you can use 'flutter_pod '#{@module_name}', '#{@version}', :mode=>'debug/release' to use the pod" end private def flutter_sdk_version_switch(version) commands = [ 'fvm', 'use', version ] if CommandRunner.run(*commands) == false # raise "Error running #{commands.join ' '} " end end def thin_arm64(path) # thin arm64 thin_commands = [ 'lipo', path, '-thin', "arm64", '-output', path, ] if CommandRunner.run(*thin_commands) == false raise "Error running #{commands.join ' '} " end end def release app_file, sdk_file = zip_files @plugins, 'Release' upload_files app_file, sdk_file, 'Release' make_pod 'Release' end def debug app_file, sdk_file = zip_files @plugins, 'Debug' upload_files app_file, sdk_file, 'Debug' make_pod 'Debug' end def pub_upgrade if CommandRunner.run('fvm', @flutter_wrapper, 'pub', 'upgrade') == false raise "Pub upgrade fail" end end def pub_get if CommandRunner.run('fvm', @flutter_wrapper, 'pub', 'get') == false raise "Pub get fail" end end def local_podfile_exists? local_podfile_dir = @working_dir + '/local_podfile' File.exist? local_podfile_dir end def setup_pod_sources Pod::UserInterface.info 'Setup pod sources...' origin_pod_file_root_dir = @working_dir + '/.ios' origin_pod_file_dir = origin_pod_file_root_dir+ '/Podfile' temp_pod_file_dir_1 = @working_dir + '/tmp1.txt' temp_pod_file_dir_2 = @working_dir + '/tmp2.txt' if File.exist? temp_pod_file_dir_1 FileUtils.remove_file temp_pod_file_dir_1 end if File.exist? temp_pod_file_dir_2 FileUtils.remove_file temp_pod_file_dir_2 end # origin_pod_file = File.open origin_pod_file_dir temp_pod_file_1 = File.open temp_pod_file_dir_1, "w+" temp_pod_file_2 = File.open temp_pod_file_dir_2, "w+" @sources.each do |s| temp_pod_file_2.puts "source '#{s}'" end lines = IO.readlines(origin_pod_file_dir) lines.each do |line| if line =~ /platform\s+:ios\s*,\s*'\s*[0-9]*.[0-9]*\s*'/ temp_pod_file_1.puts "platform :ios, '9.0'" elsif line =~ /source\s+/ Pod::UserInterface.message 'Ignore ' + line else temp_pod_file_1.puts line end end temp_pod_file_1.close lines = IO.readlines(temp_pod_file_1) lines.each do |line| temp_pod_file_2.puts line end temp_pod_file_2.close File.open origin_pod_file_dir, "w+" do |file| file.write IO.readlines(temp_pod_file_2).join('') file.close end end def use_local_podfile origin_pod_file_root_dir = @working_dir + '/.ios' origin_pod_file_dir = origin_pod_file_root_dir+ '/Podfile' if File.exist? origin_pod_file_dir File.delete(origin_pod_file_dir) end local_podfile_dir = @working_dir + '/local_podfile' FileUtils.copy_file local_podfile_dir, origin_pod_file_dir end def excute_build_run if CommandRunner.run('fvm', @flutter_wrapper, 'packages','pub','run','build_runner','build','--delete-conflicting-outputs') == false raise "Error running #{@flutter_wrapper} packages pub run build_runner build --delete-conflicting-outputs" end end def build_framework commands = [ 'fvm', @flutter_wrapper, 'build', 'ios-framework', "--output=#{@product_dir}", '--no-profile' ] if @build_modes.include?('debug') == false commands.append '--no-debug' end if @build_modes.include?('release') == false commands.append '--no-release' end if CommandRunner.run(*commands) == false # FileUtils.remove_dir product_dir, true raise "Error running #{commands.join ' '} " end end # def build_app(mode) # if CommandRunner.run('fvm', @flutter_wrapper, 'build','ios',"--#{mode}" ,"--no-codesign") == false # # FileUtils.remove_dir product_dir, true # raise "Error running #{@flutter_wrapper} build ios --#{mode} --no-codesign" # end # end def fetch_plugins results = [] flutter_plugins_list = @working_dir + '/.flutter-plugins' lines = IO.readlines(flutter_plugins_list) lines.each do |line| unless line =~ /^#/ key_val = line.split '=' key = key_val[0] value = key_val[1] value = value.gsub /\n/, '' yaml_content = YAML.load_file value + 'pubspec.yaml' f_h = yaml_content['flutter'] if f_h != nil then p_h = f_h['plugin'] if p_h != nil then pl_h = p_h['platforms'] if pl_h != nil then if pl_h['ios'] != nil path = "#{value}ios/#{key}.podspec" if File.exist? path results.append Plugins.new(key, path, Pod::Specification.from_file(path)) end end else path = "#{value}ios/#{key}.podspec" if File.exist? path results.append Plugins.new(key, path, Pod::Specification.from_file(path)) end end else path = "#{value}ios/#{key}.podspec" if File.exist? path results.append Plugins.new(key, path, Pod::Specification.from_file(path)) end end else path = "#{value}ios/#{key}.podspec" if File.exist? path results.append Plugins.new(key, path, Pod::Specification.from_file(path)) end end end end results end def zip_files(plugins, mode) product_dir = @product_dir + "#{mode}" zip_file_dir = product_dir + "/zip" if Dir.exist? zip_file_dir FileUtils.remove_dir zip_file_dir, true end FileUtils.mkdir_p zip_file_dir zip_file = zip_file_dir + "/#{@module_name}.zip" Zip::File.open(zip_file, Zip::File::CREATE) do |zipfile| stdin, stdout, stderr, wait_thr = Open3.popen3('git', 'rev-parse', 'HEAD') commit = stdout.gets.gsub /\n/, '' info_hash = { :name => @module_name, :commit => commit, :version => @version, :sdk_version => @flutter_version } json = JSON.pretty_generate info_hash zipfile.get_output_stream("info.json") { |f| f.write json } zipfile.add 'pubspec.lock', "#{@working_dir}/pubspec.lock" zipfile.get_output_stream("download_sdk.rb") { |f| f.write sdk_download } zipfile.add_dir "App.xcframework", "#{product_dir}/App.xcframework" zipfile.add_dir "FlutterPluginRegistrant.xcframework","#{product_dir}/FlutterPluginRegistrant.xcframework" plugins.each do |plugin| file = File.join(product_dir, "#{plugin.spec.name}.xcframework") if Dir.exist? file zipfile.add_dir("#{plugin.spec.name}.xcframework", file) end end end sdk_file = zip_file_dir + "/Flutter.zip" home_dir = Dir.home cache_dir = home_dir + "/.cocoapods/.cache" flutter_sdk_dsym_dir = cache_dir + "/flutter_sdk_dsym" #创建dsym缓存文件 if Dir.exist?(flutter_sdk_dsym_dir) == false Dir.mkdir flutter_sdk_dsym_dir end Zip::File.open sdk_file, Zip::File::CREATE do |zipfile| file = File.join(product_dir, "Flutter.xcframework") zipfile.add_dir "Flutter/Flutter.xcframework", file #读取Flutter.framework中的info.plist文件中的FlutterEngine的值 info_plist_path = info_plist_from_xcframework file, 1 # info_plist_path = File.join(file, "Info.plist") info_plist = Plist.parse_xml(info_plist_path) engine_hash = info_plist["FlutterEngine"] dsym_dir = File.join(flutter_sdk_dsym_dir, engine_hash) dsym_zip_file_path = File.join(dsym_dir, 'Flutter.framework.dSYM.zip') dsym_file_path = File.join(dsym_dir, 'Flutter.framework.dSYM') #检查dsym文件缓存 Pod::UserInterface.info "Checking Flutter.framework.dSYM at #{dsym_dir}" if Dir.exist?(dsym_dir) == false Pod::UserInterface.info "Flutter.framework.dSYM not found at #{dsym_dir}" Dir.mkdir(dsym_dir) unless Dir.exist?(dsym_dir) #下载dsym remote = Remote.new() remote.download_flutter_sdk_dsym @flutter_version, engine_hash, dsym_zip_file_path Pod::UserInterface.info "Flutter.framework.dSYM download success" Dir.mkdir(dsym_file_path) unless File.exist?(dsym_file_path) Pod::UserInterface.info "Start unzip #{dsym_zip_file_path}" Zip::File.open(dsym_zip_file_path) do |zif_file| zif_file.each do |entry| # 通过下句打印可知,entyr是Zip::ZipEntry的对象 # puts entry.class # 利用File.join构建文件存放的路径,路径为存放目录加上压缩文件的相对路径 entry.extract(File.join(dsym_dir, entry::name)) end end #重命名:如果文件名为Flutter.dSYM,则将Flutter.dSYM重命名为Flutter.framework.dSYM,因为cocoapods只识别Flutter.framework.dSYM File.rename( File.join( dsym_dir, 'Flutter.dSYM'), dsym_file_path ) if File.exist? File.join( dsym_dir, 'Flutter.dSYM') Pod::UserInterface.info "Unzip #{dsym_zip_file_path} success" end #向zip中添加dsym文件 Pod::UserInterface.info "Add Flutter.framework.dSYM to #{sdk_file}" zipfile.add_dir "Flutter/Flutter.framework.dSYM", dsym_file_path end [zip_file, sdk_file] end #depth 1为第一层 def info_plist_from_xcframework(path, depth) children = Dir.children path for child in children child_path = File.join path, child if File.directory? child_path then r = info_plist_from_xcframework child_path, depth + 1 if r != nil return r end elsif depth > 1 && child == 'Info.plist' then return child_path end end return nil end def sdk_download ruby_code = <<-CODE #!/usr/bin/env ruby require 'digest' require 'open-uri' sdk_url = ARGV[0] def md5(url) Digest::MD5.hexdigest url end def download(url, dest) open(url) do |u| File.open(dest, "w+") do |file| file.write u.read end end end home_dir = Dir.home cache_dir = home_dir + "/.cocoapods/.cache" if Dir.exist?(cache_dir) == false Dir.mkdir cache_dir end #SDK下载 sdk_dir = cache_dir + "/\#{md5 sdk_url}" sdk_file = sdk_dir + "/Flutter.zip" if Dir.exist?(sdk_dir) == false Dir.mkdir sdk_dir download sdk_url, sdk_file end if Dir.exist? "Flutter" `rm -rf Flutter` end `unzip \#{sdk_file} -d \#{Dir.pwd}` CODE ruby_code end def upload_files(app_file, flutter_sdk, mode) Pod::UserInterface.info 'Start upload resources' remote = Remote.new() Pod::UserInterface.info "Checking #{@module_name}_#{@version}_#{mode.downcase}..." result = remote.exist? @module_name, @version, mode.downcase if result Pod::UserInterface.info "Found #{@module_name}_#{@version}_#{mode.downcase} and deleting..." remote.delete @module_name, @version, mode.downcase end Pod::UserInterface.info "Uploading #{@module_name}_#{@version}_#{mode.downcase}..." remote.upload @module_name, @version, mode.downcase, app_file Pod::UserInterface.info "Checking Flutter_#{@flutter_version}_#{mode.downcase}..." result = remote.exist? 'Flutter', @flutter_version, mode.downcase unless result Pod::UserInterface.info "No Flutter_#{@flutter_version}_#{mode.downcase} found, start uploading..." remote.upload 'Flutter', @flutter_version, mode.downcase, flutter_sdk end if result Pod::UserInterface.info "Flutter_#{@flutter_version}_#{mode.downcase} was found, skip upload" end end def ignore_dependency?(name) if name == 'Flutter' return true end for plugin in @plugins if plugin.name == name return true end end return false end def make_pod(mode) dependency_graph = Molinillo::DependencyGraph.new @plugins.each do |plugin| plugin.spec.all_dependencies('ios').each do |dep| unless ignore_dependency? dep.name dependency_graph.add_vertex(dep.name, dep, true) end end end remote = Remote.new() sdk_download_url = remote.download_url "Flutter", @flutter_version, mode.downcase app_download_url = remote.download_url @module_name, @version, mode.downcase spec = Pod::Spec.new do |s| s.swift_version = '5.0' s.name = "#{@module_name}_#{mode.downcase}" s.version = @version s.summary = @module_name s.description = @module_name s.homepage = 'http://dreamtracer.top' s.license = { :type => 'BSD' } s.author = { 'Dreamtracer' => 'http://dreamtracer.top' } s.source = { :http => app_download_url } s.ios.deployment_target = '10.0' s.prepare_command = "ruby download_sdk.rb #{sdk_download_url}" s.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64 x86_64' } s.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64 x86_64' } vendored_frameworks = ["App.xcframework", "Flutter/Flutter.xcframework", "FlutterPluginRegistrant.xcframework"] @plugins.each do |plugin| vendored_frameworks.append "#{plugin.name}.xcframework" end s.vendored_frameworks = vendored_frameworks dependency_graph.each do |vertex| s.dependency vertex.payload.name, vertex.payload.requirement.to_s end s.resources = ['info.json', 'pubspec.lock'] end temp_dir = Dir.tmpdir spec_file = temp_dir + "/#{@module_name}_#{mode.downcase}.podspec.json" File.open spec_file, "w+" do |file| file.write spec.to_pretty_json end Pod::UserInterface.info "podspec file path #{spec_file}" Dir.chdir temp_dir do |dir| Pod::Command::Repo::Push.run([@pod_repo, '--skip-import-validation', '--verbose', '--allow-warnings', "--sources=#{@sources.join(',')}"]) File.delete spec_file end end end