├── .fasterer.yml ├── .github ├── pull_request_template.md └── workflows │ └── Test.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── fastlane-plugin-create_xcframework.gemspec ├── lib └── fastlane │ └── plugin │ ├── create_xcframework.rb │ └── create_xcframework │ ├── actions │ └── create_xcframework_action.rb │ ├── helper │ └── create_xcframework_helper.rb │ └── version.rb └── spec ├── fastlane_create_xcframework_action_spec.rb ├── fastlane_create_xcframework_helper_spec.rb └── spec_helper.rb /.fasterer.yml: -------------------------------------------------------------------------------- 1 | speedups: 2 | each_with_index_vs_while: false 3 | 4 | exclude_paths: 5 | - 'vendor/**/*.rb' 6 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## References 4 | - https://github.com/bielikb/fastlane-plugin-create_xcframework/issues/XXX 5 | 6 | ## Risks 7 | - [ ] None 8 | - [ ] Low 9 | - [ ] High 10 | -------------------------------------------------------------------------------- /.github/workflows/Test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | chat: 7 | name: Checks 8 | runs-on: macos-11 9 | timeout-minutes: 10 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: '0' 14 | - uses: actions/cache@v2.1.6 15 | id: bundler-cache 16 | with: 17 | path: vendor/bundle 18 | key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} 19 | restore-keys: | 20 | ${{ runner.os }}-gems- 21 | - name: Bundler 22 | run: | 23 | gem install bundler 24 | bundle config path vendor/bundle 25 | bundle check || bundle install --jobs 4 --retry 3 26 | - name: Tests 27 | run: bundle exec rake 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | Gemfile.lock 3 | 4 | ## Documentation cache and generated files: 5 | /.yardoc/ 6 | /_yardoc/ 7 | /doc/ 8 | /rdoc/ 9 | fastlane/README.md 10 | fastlane/report.xml 11 | coverage 12 | test-results 13 | /vendor/ 14 | .DS_Store 15 | *.xcarchive -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --color 3 | --format d 4 | --format RspecJunitFormatter 5 | --out test-results/rspec/rspec.xml 6 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | require: 3 | - rubocop/require_tools 4 | - rubocop-performance 5 | - rubocop-rspec 6 | - rubocop-rake 7 | AllCops: 8 | TargetRubyVersion: 2.4 9 | NewCops: enable 10 | Include: 11 | - "**/*.rb" 12 | - "**/*file" 13 | - "**/*.gemspec" 14 | - "*/lib/assets/*Template" 15 | - "*/lib/assets/*TemplateAndroid" 16 | Exclude: 17 | - "**/lib/assets/custom_action_template.rb" 18 | - "./vendor/**/*" 19 | - "**/lib/assets/DefaultFastfileTemplate" 20 | - "**/lib/assets/MatchfileTemplate" 21 | - "**/spec/fixtures/broken_files/broken_file.rb" 22 | - "**/*.provisionprofile" 23 | Style/MultipleComparison: 24 | Enabled: false 25 | Style/PercentLiteralDelimiters: 26 | Enabled: false 27 | Style/ClassCheck: 28 | EnforcedStyle: kind_of? 29 | Style/FrozenStringLiteralComment: 30 | Enabled: false 31 | Style/SafeNavigation: 32 | Enabled: false 33 | Performance/RegexpMatch: 34 | Enabled: false 35 | Performance/StringReplacement: 36 | Enabled: false 37 | Style/NumericPredicate: 38 | Enabled: false 39 | Metrics/BlockLength: 40 | Enabled: false 41 | Metrics/ModuleLength: 42 | Enabled: false 43 | Naming/VariableNumber: 44 | Enabled: false 45 | Naming/MethodName: 46 | IgnoredPatterns: 47 | - 'xcarchive_BCSymbolMaps_path' 48 | - 'xcframework_BCSymbolMaps_path' 49 | - 'xcframework_dSYMs_path' 50 | - 'xcarchive_dSYMs_path' 51 | - 'copy_BCSymbolMaps' 52 | - 'copy_dSYMs' 53 | Naming/VariableName: 54 | Enabled: false 55 | Style/MissingRespondToMissing: 56 | Enabled: false 57 | Style/MultilineBlockChain: 58 | Enabled: false 59 | Style/NumericLiteralPrefix: 60 | Enabled: false 61 | Style/TernaryParentheses: 62 | Enabled: false 63 | Style/EmptyMethod: 64 | Enabled: false 65 | Lint/UselessAssignment: 66 | Exclude: 67 | - "**/spec/**/*" 68 | Require/MissingRequireStatement: 69 | Enabled: false 70 | Layout/FirstHashElementIndentation: 71 | Enabled: false 72 | Layout/HashAlignment: 73 | Enabled: false 74 | Layout/DotPosition: 75 | Enabled: false 76 | Style/DoubleNegation: 77 | Enabled: false 78 | Style/SymbolArray: 79 | Enabled: false 80 | Layout/HeredocIndentation: 81 | Enabled: false 82 | Style/MixinGrouping: 83 | Exclude: 84 | - "**/spec/**/*" 85 | Lint/OrAssignmentToConstant: 86 | Enabled: false 87 | Lint/SuppressedException: 88 | Enabled: false 89 | Lint/UnusedBlockArgument: 90 | Enabled: false 91 | Lint/AmbiguousBlockAssociation: 92 | Enabled: false 93 | Style/GlobalVars: 94 | Enabled: false 95 | Style/ClassAndModuleChildren: 96 | Enabled: false 97 | Style/SpecialGlobalVars: 98 | Enabled: false 99 | Metrics/AbcSize: 100 | Enabled: false 101 | Metrics/MethodLength: 102 | Enabled: false 103 | Metrics/CyclomaticComplexity: 104 | Enabled: false 105 | Style/WordArray: 106 | MinSize: 19 107 | Style/SignalException: 108 | Enabled: false 109 | Style/RedundantReturn: 110 | Enabled: false 111 | Style/IfUnlessModifier: 112 | Enabled: false 113 | Style/AndOr: 114 | Enabled: true 115 | EnforcedStyle: conditionals 116 | Metrics/ClassLength: 117 | Max: 320 118 | Layout/LineLength: 119 | Max: 370 120 | Metrics/ParameterLists: 121 | Max: 17 122 | Metrics/PerceivedComplexity: 123 | Max: 20 124 | Style/GuardClause: 125 | Enabled: false 126 | Style/StringLiterals: 127 | Enabled: false 128 | Style/ConditionalAssignment: 129 | Enabled: false 130 | Style/RedundantSelf: 131 | Enabled: false 132 | Lint/UnusedMethodArgument: 133 | Enabled: false 134 | Lint/ParenthesesAsGroupedExpression: 135 | Exclude: 136 | - "**/spec/**/*" 137 | Naming/PredicateName: 138 | Enabled: false 139 | Style/PerlBackrefs: 140 | Enabled: false 141 | Layout/SpaceAroundOperators: 142 | Exclude: 143 | - "**/spec/actions_specs/xcodebuild_spec.rb" 144 | Naming/FileName: 145 | Exclude: 146 | - "**/Dangerfile" 147 | - "**/Brewfile" 148 | - "**/Gemfile" 149 | - "**/Podfile" 150 | - "**/Rakefile" 151 | - "**/Fastfile" 152 | - "**/Deliverfile" 153 | - "**/Snapfile" 154 | - "**/Pluginfile" 155 | - "**/*.gemspec" 156 | Style/Documentation: 157 | Enabled: false 158 | Style/MutableConstant: 159 | Enabled: false 160 | Style/ZeroLengthPredicate: 161 | Enabled: false 162 | Style/IfInsideElse: 163 | Enabled: false 164 | Style/CollectionMethods: 165 | Enabled: false 166 | Style/MethodCallWithArgsParentheses: 167 | Enabled: true 168 | IgnoredMethods: 169 | - require 170 | - require_relative 171 | - fastlane_require 172 | - gem 173 | - program 174 | - command 175 | - raise 176 | - attr_accessor 177 | - attr_reader 178 | - desc 179 | - lane 180 | - private_lane 181 | - platform 182 | - to 183 | - not_to 184 | - describe 185 | - it 186 | - be 187 | - context 188 | - before 189 | - after 190 | - and 191 | RSpec/ExampleLength: 192 | Max: 20 193 | RSpec/MultipleMemoizedHelpers: 194 | Max: 10 195 | RSpec/MessageSpies: 196 | Enabled: false 197 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at h3sperian@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to fastlane plugin create_xcframework 2 | 3 | First of all, we want to thank you for you interest in this project. 4 | Every contribution matters, this is how we are keeping the project alive & the plugin up to date with the latest versions of Xcode. 5 | We are all owners, from Boris Bielik & Alexey Alter-Pesotskiy who initially created the plugin, to the people who use the plugin in their projects and participated in a conversation, improved the documentation, created an example or made any code changes. 6 | If you want to help, but don't know where to begin, we recommend you start small: 7 | - it would help a lot if you read: https://opensource.guide/how-to-contribute/ 8 | - read the ongoing discussions to see what the overall status is 9 | - pitch in the converstations where you feel it's useful. Talk to other people, ask questions if you don't understand something 10 | - contributing to the documentation or the ongoing converstations is as important as contributing to the code 11 | 12 | We want to make contributing to this project as easy and transparent as possible. Here are a few guidelines for making all our lives easier. 13 | 14 | ## Contribute to ongoing conversations 15 | 16 | If you have a valid opinion about something we are talking about, feel free to share it, either by opening an issue or the pull request against this repo. 17 | If you also encountered an issue that someone else described, pitch in. Better yet, if you already fixed it in a certain way, share that. 18 | 19 | ## Improving documentation 20 | 21 | A project's documentation is something you can always improve on. Make sure you go through the existing documentation and suggest changes or just create new pieces of documentation. 22 | Examples are also welcome. 23 | 24 | ## Reporting Issues 25 | 26 | A great way to contribute to the project is to send a detailed issue when you encounter an problem. 27 | It is very important to check for the same problem or suggestion in the project's issue list first. If you find a match, just add a small comment there. 28 | Doing this helps prioritize the most common problems and requests. 29 | 30 | When reporting issues, please include the following: 31 | 32 | - The lane that uses the plugin 33 | - The plugin version 34 | - The Swift version/Swift toolchain being used. 35 | - The version of Xcode you're using 36 | - The full output of any stack trace or compiler error 37 | - A small demo project that replicates the issue (especially if the way to reproduce the issue is not straight-forward) 38 | - Any other details that would be useful in understanding the problem 39 | 40 | This information will help us review and fix your issue faster. 41 | 42 | Please do not be offended if we close your issue and reference this document. 43 | If you believe the issue is truely a fault in the project’s codebase, re-open it. 44 | 45 | ## Pull Requests 46 | 47 | We gladly accept any PR's assuming they are well written, documented ( if necessary ) and preferably have test code. 48 | If you're unsure if we'll accept a new feature please open an issue requesting it and we can have a discussion before you code and submit a PR. 49 | 50 | Checklist: 51 | - Fork the repo and create your branch from the latest master (to minimize the conflicts) 52 | - If you've added code that should be tested, add tests. 53 | - If you've changed APIs, update the documentation. 54 | - Ensure the test suite passes. 55 | 56 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source('https://rubygems.org') 2 | 3 | gemspec 4 | 5 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 6 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Boris Bielik 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create_xcframework plugin 2 | 3 | [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-create_xcframework) 4 | 5 | ## About create_xcframework 6 | 7 | Fastlane plugin that creates xcframework for given list of destinations 🚀 8 | 9 | ## Requirements 10 | 11 | * Xcode 11.x or greater. Download it at the [Apple Developer - Downloads](https://developer.apple.com/downloads) or the [Mac App Store](https://apps.apple.com/us/app/xcode/id497799835?mt=12). 12 | * fastlane 13 | 14 | ## Getting Started 15 | 16 | To get started with `create_xcframework` plugin, add it to your project by running: 17 | 18 | ```bash 19 | $ fastlane add_plugin create_xcframework 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```ruby 25 | create_xcframework( 26 | workspace: 'path/to/your.xcworkspace', 27 | scheme: 'framework scheme', 28 | product_name: 'Sample', # optional if scheme doesnt match the name of your framework 29 | destinations: ['iOS', 'maccatalyst'], 30 | xcframework_output_directory: 'path/to/your/output dir' 31 | ) 32 | ``` 33 | 34 | Run 35 | ```bash 36 | $ fastlane actions create_xcframework 37 | ``` 38 | to learn more about the plugin. 39 | 40 | ### Supported destinations 41 | 42 | * iOS 43 | * iPadOS 44 | * maccatalyst 45 | * tvOS 46 | * watchOS 47 | * carPlayOS 48 | * macOS 49 | * visionOS 50 | 51 | ## Output 52 | 53 | #### Files: 54 | * xcframework 55 | * dSYMs dir 56 | * BCSymbolMaps dir (if bitcode is enabled) 57 | 58 | #### Env vars: 59 | * XCFRAMEWORK_OUTPUT_PATH 60 | * XCFRAMEWORK_DSYM_OUTPUT_PATH 61 | * XCFRAMEWORK_BCSYMBOLMAPS_OUTPUT_PATH 62 | 63 | 64 | ## Contribution 65 | 66 | - If you **want to contribute**, read the [Contributing Guide](https://github.com/bielikb/fastlane-plugin-create_xcframework/blob/master/CONTRIBUTING.md) 67 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | require 'rspec/core/rake_task' 4 | RSpec::Core::RakeTask.new 5 | 6 | require 'rubocop/rake_task' 7 | RuboCop::RakeTask.new(:rubocop) 8 | 9 | desc 'Run Fasterer' 10 | task :fasterer do 11 | sh('bundle exec fasterer') 12 | end 13 | 14 | task(default: [:rubocop, :fasterer, :spec]) 15 | -------------------------------------------------------------------------------- /fastlane-plugin-create_xcframework.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("lib", __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'fastlane/plugin/create_xcframework/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'fastlane-plugin-create_xcframework' 7 | spec.version = Fastlane::CreateXcframework::VERSION 8 | spec.summary = "Fastlane plugin that creates xcframework for given list of destinations." 9 | spec.description = "Fastlane plugin that creates xcframework for given list of destinations." 10 | spec.homepage = "https://github.com/bielikb/fastlane-plugin-create_xcframework" 11 | spec.license = "MIT" 12 | spec.authors = ["Boris Bielik", "Alexey Alter-Pesotskiy"] 13 | spec.email = ["bielik.boris@gmail.com", "a.alterpesotskiy@mail.ru"] 14 | 15 | spec.files = Dir["lib/**/*"] + %w(README.md LICENSE) 16 | spec.bindir = "bin" 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ['lib'] 19 | 20 | spec.required_ruby_version = '>= 2.6' 21 | spec.add_development_dependency('bundler') 22 | spec.add_development_dependency('fasterer', '0.9.0') 23 | spec.add_development_dependency('fastlane', '>= 2.182.0') 24 | spec.add_development_dependency('pry') 25 | spec.add_development_dependency('rake') 26 | spec.add_development_dependency('rspec') 27 | spec.add_development_dependency('rspec_junit_formatter') 28 | spec.add_development_dependency('rubocop', '1.12.1') 29 | spec.add_development_dependency('rubocop-performance') 30 | spec.add_development_dependency('rubocop-rake', '0.6.0') 31 | spec.add_development_dependency('rubocop-require_tools') 32 | spec.add_development_dependency('rubocop-rspec', '2.4.0') 33 | spec.add_development_dependency('simplecov') 34 | end 35 | -------------------------------------------------------------------------------- /lib/fastlane/plugin/create_xcframework.rb: -------------------------------------------------------------------------------- 1 | require 'fastlane/plugin/create_xcframework/version' 2 | 3 | module Fastlane 4 | module CreateXcframework 5 | # Return all .rb files inside the "actions" and "helper" directory 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 we want to import all available actions and helpers 13 | # A plugin can contain any number of actions and plugins 14 | Fastlane::CreateXcframework.all_classes.each do |current| 15 | require current 16 | end 17 | -------------------------------------------------------------------------------- /lib/fastlane/plugin/create_xcframework/actions/create_xcframework_action.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Actions 3 | module SharedValues 4 | XCFRAMEWORK_OUTPUT_PATH ||= :XCFRAMEWORK_OUTPUT_PATH 5 | XCFRAMEWORK_DSYM_OUTPUT_PATH ||= :XCFRAMEWORK_DSYM_OUTPUT_PATH 6 | XCFRAMEWORK_BCSYMBOLMAPS_OUTPUT_PATH ||= :XCFRAMEWORK_BCSYMBOLMAPS_OUTPUT_PATH 7 | end 8 | 9 | require 'fastlane_core/ui/ui' 10 | require 'fastlane/actions/xcodebuild' 11 | require_relative '../helper/create_xcframework_helper' 12 | 13 | class CreateXcframeworkAction < Action 14 | def self.run(params) 15 | if Helper.xcode_at_least?('11.0.0') 16 | verify_delicate_params(params) 17 | params[:destinations] = update_destinations(params) 18 | params[:xcargs] = update_xcargs(params) 19 | 20 | @xchelper = Helper::CreateXcframeworkHelper.new(params) 21 | 22 | params[:destinations].each_with_index do |destination, framework_index| 23 | options = params.values 24 | options[:destination] = destination 25 | options[:archive_path] = @xchelper.xcarchive_path_for_destination(framework_index) 26 | XcarchiveAction.run(options) 27 | end 28 | 29 | create_xcframework(params) 30 | 31 | copy_dSYMs(params) 32 | 33 | copy_BCSymbolMaps(params) 34 | 35 | clean(params) 36 | 37 | provide_shared_values 38 | else 39 | UI.important('xcframework can be produced only using Xcode 11 and above') 40 | end 41 | end 42 | 43 | def self.provide_shared_values 44 | Actions.lane_context[SharedValues::XCFRAMEWORK_OUTPUT_PATH] = File.expand_path(@xchelper.xcframework_path) 45 | ENV[SharedValues::XCFRAMEWORK_OUTPUT_PATH.to_s] = File.expand_path(@xchelper.xcframework_path) 46 | Actions.lane_context[SharedValues::XCFRAMEWORK_DSYM_OUTPUT_PATH] = File.expand_path(@xchelper.xcframework_dSYMs_path) 47 | ENV[SharedValues::XCFRAMEWORK_DSYM_OUTPUT_PATH.to_s] = File.expand_path(@xchelper.xcframework_dSYMs_path) 48 | Actions.lane_context[SharedValues::XCFRAMEWORK_BCSYMBOLMAPS_OUTPUT_PATH] = File.expand_path(@xchelper.xcframework_BCSymbolMaps_path) 49 | ENV[SharedValues::XCFRAMEWORK_BCSYMBOLMAPS_OUTPUT_PATH.to_s] = File.expand_path(@xchelper.xcframework_BCSymbolMaps_path) 50 | end 51 | 52 | def self.clean(params) 53 | FileUtils.rm_rf(@xchelper.xcarchive_path) if params[:remove_xcarchives] 54 | end 55 | 56 | def self.create_xcframework(params) 57 | xcframework = @xchelper.xcframework_path 58 | begin 59 | FileUtils.rm_rf(xcframework) if File.exist?(xcframework) 60 | 61 | arguments = ['-create-xcframework'] 62 | arguments << '-allow-internal-distribution' if params[:allow_internal_distribution] 63 | params[:destinations].each_with_index do |_, index| 64 | arguments << "-framework #{@xchelper.xcarchive_framework_path(index)}" 65 | arguments << debug_symbols(index: index, params: params) 66 | end 67 | arguments << "-output #{xcframework}" 68 | 69 | Actions.sh("set -o pipefail && xcodebuild #{arguments.reject(&:empty?).join(' ')}") 70 | rescue StandardError => e 71 | UI.user_error!(e) 72 | end 73 | end 74 | 75 | def self.debug_symbols(index:, params:) 76 | return '' if !Helper.xcode_at_least?('12.0.0') || params[:include_debug_symbols] == false 77 | 78 | debug_symbols = [] 79 | 80 | # Include dSYMs in xcframework 81 | if params[:include_dSYMs] != false 82 | debug_symbols << "-debug-symbols #{@xchelper.xcarchive_dSYMs_path(index)}/#{@xchelper.framework}.dSYM" 83 | end 84 | 85 | # Include BCSymbols in xcframework 86 | if params[:include_BCSymbolMaps] != false && params[:enable_bitcode] != false 87 | bc_symbols_dir = @xchelper.xcarchive_BCSymbolMaps_path(index) 88 | if Dir.exist?(bc_symbols_dir) 89 | arguments = Dir.children(bc_symbols_dir).map { |path| "-debug-symbols #{File.expand_path("#{bc_symbols_dir}/#{path}")}" } 90 | debug_symbols << arguments.join(' ') 91 | end 92 | end 93 | 94 | debug_symbols.join(' ') 95 | end 96 | 97 | def self.copy_dSYMs(params) 98 | return if params[:include_debug_symbols] == false 99 | 100 | dSYMs_output_dir = @xchelper.xcframework_dSYMs_path 101 | FileUtils.mkdir_p(dSYMs_output_dir) 102 | 103 | params[:destinations].each_with_index do |_, framework_index| 104 | dSYM_source = "#{@xchelper.xcarchive_dSYMs_path(framework_index)}/#{@xchelper.framework}.dSYM" 105 | identifier = @xchelper.library_identifier(framework_index) 106 | dSYM = "#{@xchelper.framework}.#{identifier}.dSYM" 107 | dSYM_destination = "#{dSYMs_output_dir}/#{dSYM}" 108 | 109 | UI.important("▸ Copying #{dSYM} to #{dSYMs_output_dir}") 110 | FileUtils.cp_r(dSYM_source, dSYM_destination) 111 | end 112 | end 113 | 114 | def self.copy_BCSymbolMaps(params) 115 | return if params[:include_debug_symbols] == false || params[:enable_bitcode] == false || params[:include_BCSymbolMaps] == false 116 | 117 | symbols_output_dir = @xchelper.xcframework_BCSymbolMaps_path 118 | FileUtils.mkdir_p(symbols_output_dir) 119 | 120 | params[:destinations].each_with_index do |_, framework_index| 121 | symbols_xcarchive_dir = @xchelper.xcarchive_BCSymbolMaps_path(framework_index) 122 | next unless Dir.exist?(symbols_xcarchive_dir) 123 | 124 | FileUtils.cp_r("#{symbols_xcarchive_dir}/.", symbols_output_dir) 125 | UI.important("▸ Copying #{Dir.children(symbols_xcarchive_dir)} to #{symbols_output_dir}") 126 | end 127 | end 128 | 129 | def self.verify_delicate_params(params) 130 | UI.user_error!('Error: :scheme is required option') if params[:scheme].nil? 131 | if !params[:destinations].nil? && !params[:destinations].kind_of?(Array) 132 | UI.user_error!('Error: :destinations option should be presented as Array') 133 | end 134 | end 135 | 136 | def self.update_xcargs(params) 137 | xcargs = [] 138 | if params[:override_xcargs] 139 | UI.important('Overwriting SKIP_INSTALL and BUILD_LIBRARY_FOR_DISTRIBUTION options') 140 | if params[:xcargs] 141 | params[:xcargs].gsub!(/SKIP_INSTALL(=|\s+)(YES|NO)/, '') 142 | params[:xcargs].gsub!(/BUILD_LIBRARY_FOR_DISTRIBUTION(=|\s+)(YES|NO)/, '') 143 | end 144 | xcargs.concat(['SKIP_INSTALL=NO', 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES']) 145 | end 146 | 147 | if params[:enable_bitcode] != false 148 | params[:xcargs].gsub!(/ENABLE_BITCODE(=|\s+)(YES|NO)/, '') if params[:xcargs] 149 | xcargs << ['OTHER_CFLAGS="-fembed-bitcode"', 'BITCODE_GENERATION_MODE="bitcode"', 'ENABLE_BITCODE=YES'] 150 | end 151 | 152 | "#{params[:xcargs].to_s.strip} #{xcargs.join(' ')}" 153 | end 154 | 155 | def self.update_destinations(params) 156 | return available_destinations.values[0] if params[:destinations].nil? 157 | 158 | requested_destinations = params[:destinations].map do |requested_de| 159 | available_destinations.select { |available_de, _| available_de == requested_de }.values 160 | end 161 | UI.user_error!("Error: available destinations: #{available_destinations.keys}") if requested_destinations.any?(&:empty?) 162 | 163 | requested_destinations.flatten 164 | end 165 | 166 | def self.available_destinations 167 | { 168 | 'iOS' => ['generic/platform=iOS', 'generic/platform=iOS Simulator'], 169 | 'iPadOS' => ['generic/platform=iPadOS', 'generic/platform=iPadOS Simulator'], 170 | 'tvOS' => ['generic/platform=tvOS', 'generic/platform=tvOS Simulator'], 171 | 'watchOS' => ['generic/platform=watchOS', 'generic/platform=watchOS Simulator'], 172 | 'carPlayOS' => ['generic/platform=carPlayOS', 'generic/platform=carPlayOS Simulator'], 173 | 'macOS' => ['generic/platform=macOS'], 174 | 'maccatalyst' => ['generic/platform=macOS,variant=Mac Catalyst'], 175 | 'visionOS' => ['generic/platform=visionOS', 'generic/platform=visionOS Simulator'] 176 | } 177 | end 178 | 179 | ##################################################### 180 | # Documentation # 181 | ##################################################### 182 | 183 | def self.description 184 | 'Fastlane plugin that creates xcframework for given list of destinations.' 185 | end 186 | 187 | def self.example_code 188 | [ 189 | create_xcframework( 190 | workspace: 'path/to/your.xcworkspace', 191 | scheme: 'framework scheme', 192 | destinations: ['iOS'], 193 | xcframework_output_directory: 'output_directory' 194 | ) 195 | ] 196 | end 197 | 198 | def self.output 199 | [ 200 | ['XCFRAMEWORK_OUTPUT_PATH', 'The path to the newly generated xcframework'], 201 | ['XCFRAMEWORK_DSYM_OUTPUT_PATH', 'The path to the folder with dSYMs'], 202 | ['XCFRAMEWORK_BCSYMBOLMAPS_OUTPUT_PATH', 'The path to the folder with BCSymbolMaps'] 203 | ] 204 | end 205 | 206 | def self.authors 207 | ['Boris Bielik', 'Alexey Alter-Pesotskiy'] 208 | end 209 | 210 | def self.details 211 | 'Create xcframework plugin generates xcframework for specified destinations. ' \ 212 | 'The output of this action consists of the xcframework itself, which contains dSYM and BCSymbolMaps, if bitcode is enabled.' 213 | end 214 | 215 | def self.available_options 216 | XcarchiveAction.available_options.select{ |item| item[0] != 'scheme' }.map { |elem| 217 | FastlaneCore::ConfigItem.new( 218 | key: elem[0].to_sym, 219 | description: elem[1].delete_suffix('.'), 220 | optional: true) 221 | } + [ 222 | FastlaneCore::ConfigItem.new( 223 | key: :project, 224 | description: "The Xcode project to work with", 225 | optional: true 226 | ), 227 | FastlaneCore::ConfigItem.new( 228 | key: :scheme, 229 | description: "The project's scheme. Make sure it's marked as Shared", 230 | optional: false 231 | ), 232 | FastlaneCore::ConfigItem.new( 233 | key: :enable_bitcode, 234 | description: 'Should the project be built with bitcode enabled?', 235 | type: Boolean, 236 | default_value: false 237 | ), 238 | FastlaneCore::ConfigItem.new( 239 | key: :destinations, 240 | description: 'Use custom destinations for building the xcframework', 241 | type: Array, 242 | default_value: ['iOS'] 243 | ), 244 | FastlaneCore::ConfigItem.new( 245 | key: :xcframework_output_directory, 246 | description: 'The directory in which the xcframework should be stored in', 247 | optional: true 248 | ), 249 | FastlaneCore::ConfigItem.new( 250 | key: :include_dSYMs, 251 | description: 'Includes dSYM files in the xcframework', 252 | type: Boolean, 253 | default_value: true 254 | ), 255 | FastlaneCore::ConfigItem.new( 256 | key: :include_BCSymbolMaps, 257 | description: 'Includes BCSymbolMap files in the xcframework', 258 | type: Boolean, 259 | default_value: false 260 | ), 261 | FastlaneCore::ConfigItem.new( 262 | key: :include_debug_symbols, 263 | description: 'This feature was added in Xcode 12.0.' \ 264 | 'If this is set to false, the dSYMs and BCSymbolMaps wont be added to XCFramework itself', 265 | type: Boolean, 266 | default_value: true 267 | ), 268 | FastlaneCore::ConfigItem.new( 269 | key: :product_name, 270 | description: 'The name of your module. Optional if equals to :scheme. Equivalent to CFBundleName', 271 | optional: true 272 | ), 273 | FastlaneCore::ConfigItem.new( 274 | key: :remove_xcarchives, 275 | description: 'This option will auto-remove the xcarchive files once the plugin finishes.' \ 276 | 'Set this to false to preserve the xcarchives', 277 | type: Boolean, 278 | default_value: false 279 | ), 280 | FastlaneCore::ConfigItem.new( 281 | key: :allow_internal_distribution, 282 | description: 'This option will create an xcframework with the allow-internal-distribution flag.' \ 283 | 'Allows the usage of @testable when importing the created xcframework in tests', 284 | type: Boolean, 285 | default_value: false 286 | ), 287 | FastlaneCore::ConfigItem.new( 288 | key: :override_xcargs, 289 | description: 'This option will override xcargs SKIP_INSTALL and BUILD_LIBRARY_FOR_DISTRIBUTION.' \ 290 | 'If set to true, SKIP_INSTALL will be set to NO and BUILD_LIBRARY_FOR_DISTRIBUTION will be set to YES.' \ 291 | 'Set this to false to preserve the passed xcargs', 292 | type: Boolean, 293 | default_value: true 294 | ) 295 | ] 296 | end 297 | 298 | def self.category 299 | :building 300 | end 301 | 302 | def self.is_supported?(platform) 303 | [:ios, :mac].include?(platform) 304 | end 305 | end 306 | end 307 | end 308 | -------------------------------------------------------------------------------- /lib/fastlane/plugin/create_xcframework/helper/create_xcframework_helper.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Helper 3 | class CreateXcframeworkHelper 4 | def initialize(params) 5 | @params = params 6 | end 7 | 8 | def product_name 9 | @params[:product_name] ||= @params[:scheme] 10 | end 11 | 12 | def xcframework 13 | "#{product_name}.xcframework" 14 | end 15 | 16 | def framework 17 | "#{product_name}.framework" 18 | end 19 | 20 | def xcarchive_path 21 | "#{output_directory}/archives" 22 | end 23 | 24 | def xcarchive_path_for_destination(framework_index) 25 | "#{xcarchive_path}/#{framework_index}_#{product_name}.xcarchive" 26 | end 27 | 28 | def xcarchive_framework_path(framework_index) 29 | framework_path = "#{xcarchive_path_for_destination(framework_index)}/Products/Library/Frameworks/#{framework}" 30 | return framework_path if File.exist?(framework_path) 31 | 32 | UI.user_error!("▸ PRODUCT_NAME was misdefined: `#{product_name}`. Please, provide :product_name option") 33 | end 34 | 35 | def xcarchive_frameworks_path 36 | @params[:destinations].each_with_index.map { |_, i| xcarchive_framework_path(i) } 37 | end 38 | 39 | def xcarchive_dSYMs_path(framework_index) 40 | File.expand_path("#{xcarchive_path_for_destination(framework_index)}/dSYMS") 41 | end 42 | 43 | def xcframework_dSYMs_path 44 | File.expand_path("#{output_directory}/#{product_name}.dSYMs") 45 | end 46 | 47 | def xcarchive_BCSymbolMaps_path(framework_index) 48 | File.expand_path("#{xcarchive_path_for_destination(framework_index)}/BCSymbolMaps") 49 | end 50 | 51 | def xcframework_BCSymbolMaps_path 52 | File.expand_path("#{output_directory}/#{product_name}.BCSymbolMaps") 53 | end 54 | 55 | def xcframework_path 56 | File.expand_path("#{output_directory}/#{xcframework}") 57 | end 58 | 59 | def output_directory 60 | @params[:xcframework_output_directory] || '' 61 | end 62 | 63 | def library_identifier(framework_index) 64 | framework_path = xcarchive_framework_path(framework_index) 65 | framework_basename = framework_path.split('/').last 66 | framework_root = framework_basename.split('.').first 67 | library_identifiers = Dir.chdir(xcframework_path) do 68 | Dir.glob('*').select { |f| File.directory?(f) } 69 | end 70 | library_identifier = library_identifiers.detect do |id| 71 | FileUtils.compare_file( 72 | "#{framework_path}/#{framework_root}", 73 | "#{xcframework_path}/#{id}/#{framework_basename}/#{framework_root}" 74 | ) 75 | end 76 | UI.user_error!("Error: #{xcframework_path} doesn't contain #{framework_path}") if library_identifier.nil? 77 | 78 | library_identifier 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/fastlane/plugin/create_xcframework/version.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module CreateXcframework 3 | VERSION = "1.1.2" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/fastlane_create_xcframework_action_spec.rb: -------------------------------------------------------------------------------- 1 | describe Fastlane do 2 | describe Fastlane::Actions::CreateXcframeworkAction do 3 | describe 'Action Test Suite' do 4 | let(:scheme) { 'iOSModule' } 5 | let(:destinations) { ['iOS', 'macOS'] } 6 | let(:bitcode) { 'OTHER_CFLAGS="-fembed-bitcode" BITCODE_GENERATION_MODE="bitcode" ENABLE_BITCODE=YES' } 7 | 8 | it 'verifies available_destinations method' do 9 | expected_destinations = [ 10 | 'iOS', 'iPadOS', 'macOS', 'tvOS', 'watchOS', 'carPlayOS', 'maccatalyst', 'visionOS' 11 | ] 12 | actual_destinations = described_class.available_destinations.keys 13 | expect(actual_destinations.sort).to eq(expected_destinations.sort) 14 | end 15 | 16 | it 'verifies update_destinations method' do 17 | params = { destinations: destinations } 18 | generics = described_class.available_destinations.select do |k, _| 19 | destinations.include?(k) 20 | end.values.flatten 21 | result = described_class.update_destinations(params) 22 | expect(result).to eq(generics) 23 | end 24 | 25 | it 'verifies update_destinations method when no destination is provided' do 26 | params = { destinations: nil } 27 | generics = described_class.available_destinations.select do |k, _| 28 | k == destinations.first 29 | end.values.flatten 30 | result = described_class.update_destinations(params) 31 | expect(result).to eq(generics) 32 | end 33 | 34 | it 'verifies update_destinations method when wrong destination is provided' do 35 | unsupported_destination = 'Android' 36 | params = { destinations: [destinations.first, unsupported_destination] } 37 | err = "Error: available destinations: #{described_class.available_destinations.keys}" 38 | expect { described_class.update_destinations(params) }.to raise_error(err) 39 | end 40 | 41 | it 'verifies verify_delicate_params method when :scheme option was not provided' do 42 | params = { scheme: nil } 43 | err = 'Error: :scheme is required option' 44 | expect { described_class.verify_delicate_params(params) }.to raise_error(err) 45 | end 46 | 47 | it 'verifies verify_delicate_params method when :destinations option is not Array' do 48 | params = { scheme: scheme, destinations: destinations.first } 49 | err = 'Error: :destinations option should be presented as Array' 50 | expect { described_class.verify_delicate_params(params) }.to raise_error(err) 51 | end 52 | 53 | it 'verifies verify_delicate_params method when :destinations option was not provided' do 54 | params = { scheme: scheme, destinations: nil } 55 | expect { described_class.verify_delicate_params(params) }.not_to raise_error 56 | end 57 | 58 | it 'verifies clean method' do 59 | test_data = 'test' 60 | params = { remove_xcarchives: true } 61 | allow(FileUtils).to receive(:rm_rf).and_return(test_data) 62 | allow(nil).to receive(:xcarchive_path).and_return(test_data) 63 | result = described_class.clean(params) 64 | expect(result).to eq(test_data) 65 | end 66 | 67 | it 'verifies clean method when :remove_xcarchives options equals to false' do 68 | params = { remove_xcarchives: false } 69 | result = described_class.clean(params) 70 | expect(result).to eq(nil) 71 | end 72 | 73 | it 'verifies provide_shared_values method' do 74 | allow(nil).to receive(:xcframework_path) 75 | allow(nil).to receive(:xcframework_dSYMs_path) 76 | allow(nil).to receive(:xcframework_BCSymbolMaps_path) 77 | expected_result = ['0', '1', '2'] 78 | allow(File).to receive(:expand_path).and_return( 79 | expected_result[0], expected_result[0], 80 | expected_result[1], expected_result[1], 81 | expected_result[2], expected_result[2] 82 | ) 83 | described_class.provide_shared_values 84 | result = [ 85 | ENV['XCFRAMEWORK_OUTPUT_PATH'], 86 | ENV['XCFRAMEWORK_DSYM_OUTPUT_PATH'], 87 | ENV['XCFRAMEWORK_BCSYMBOLMAPS_OUTPUT_PATH'] 88 | ] 89 | expect(result).to eq(expected_result) 90 | end 91 | 92 | it 'verifies copy_BCSymbolMaps method when all params are equals to false' do 93 | params = { enable_bitcode: false, include_BCSymbolMaps: false } 94 | described_class.copy_BCSymbolMaps(params) 95 | expect(FileUtils).not_to receive(:mkdir_p) 96 | end 97 | 98 | it 'verifies copy_BCSymbolMaps method when :enable_bitcode option is equals to true' do 99 | allow(nil).to receive(:xcframework_BCSymbolMaps_path) 100 | allow(nil).to receive(:xcarchive_BCSymbolMaps_path) 101 | allow(Dir).to receive(:exist?).and_return(true) 102 | allow(Dir).to receive(:children).and_return('test') 103 | allow(FileUtils).to receive(:mkdir_p) 104 | allow(FileUtils).to receive(:cp_r) 105 | 106 | params = { destinations: destinations, enable_bitcode: true } 107 | result = described_class.copy_BCSymbolMaps(params) 108 | expect(result).to eq(destinations) 109 | end 110 | 111 | it 'verifies copy_BCSymbolMaps method when :include_BCSymbolMaps option is equals to true' do 112 | allow(nil).to receive(:xcframework_BCSymbolMaps_path) 113 | allow(nil).to receive(:xcarchive_BCSymbolMaps_path) 114 | allow(Dir).to receive(:exist?).and_return(true) 115 | allow(Dir).to receive(:children).and_return('test') 116 | allow(FileUtils).to receive(:mkdir_p) 117 | allow(FileUtils).to receive(:cp_r) 118 | 119 | params = { destinations: destinations, include_BCSymbolMaps: true } 120 | result = described_class.copy_BCSymbolMaps(params) 121 | expect(result).to eq(destinations) 122 | end 123 | 124 | it 'verifies copy_BCSymbolMaps method when symbols_xcarchive_dir was not created' do 125 | allow(nil).to receive(:xcframework_BCSymbolMaps_path) 126 | allow(nil).to receive(:xcarchive_BCSymbolMaps_path) 127 | allow(Dir).to receive(:exist?).and_return(false) 128 | allow(FileUtils).to receive(:mkdir_p) 129 | 130 | params = { destinations: destinations, enable_bitcode: true } 131 | result = described_class.copy_BCSymbolMaps(params) 132 | expect(result).to eq(destinations) 133 | end 134 | 135 | it 'verifies copy_dSYMs method when :include_dSYMs option is equals to false' do 136 | params = { include_debug_symbols: false } 137 | described_class.copy_dSYMs(params) 138 | expect(FileUtils).not_to receive(:mkdir_p) 139 | end 140 | 141 | it 'verifies copy_dSYMs method' do 142 | allow(nil).to receive(:xcframework_dSYMs_path) 143 | allow(nil).to receive(:xcarchive_dSYMs_path) 144 | allow(nil).to receive(:library_identifier) 145 | allow(nil).to receive(:framework) 146 | allow(FileUtils).to receive(:mkdir_p) 147 | allow(FileUtils).to receive(:cp_r) 148 | 149 | params = { include_debug_symbols: true, destinations: ['iOS'] } 150 | described_class.copy_dSYMs(params) 151 | expect(FileUtils).not_to receive(:mkdir_p) 152 | end 153 | 154 | it 'verifies update_xcargs method when :override_xcargs option was provided' do 155 | params = { override_xcargs: true } 156 | result = described_class.update_xcargs(params) 157 | expected_result = " SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES #{bitcode}" 158 | expect(result).to eq(expected_result) 159 | end 160 | 161 | it 'verifies update_xcargs method when :override_xcargs and :xcargs options were provided' do 162 | xcargs = 'TESTME=33' 163 | params = { override_xcargs: true, xcargs: xcargs } 164 | result = described_class.update_xcargs(params) 165 | expected_result = "#{xcargs} SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES #{bitcode}" 166 | expect(result).to eq(expected_result) 167 | end 168 | 169 | it 'verifies update_xcargs method when fragile args have to be overwritten' do 170 | xcargs = 'SKIP_INSTALL=YES BUILD_LIBRARY_FOR_DISTRIBUTION YES ENABLE_BITCODE=NO' 171 | params = { override_xcargs: true, xcargs: xcargs } 172 | result = described_class.update_xcargs(params) 173 | expected_result = " SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES #{bitcode}" 174 | expect(result).to eq(expected_result) 175 | end 176 | 177 | it 'verifies update_xcargs method when :override_xcargs option was not provided' do 178 | xcargs = 'SKIP_INSTALL=YES BUILD_LIBRARY_FOR_DISTRIBUTION YES' 179 | params = { override_xcargs: nil, xcargs: xcargs } 180 | result = described_class.update_xcargs(params) 181 | expected_result = "#{xcargs} #{bitcode}" 182 | expect(result).to eq(expected_result) 183 | end 184 | 185 | it 'verifies update_xcargs method when :override_xcargs option is equal to false' do 186 | params = { enable_bitcode: false } 187 | result = described_class.update_xcargs(params) 188 | expect(result).to eq(' ') 189 | end 190 | 191 | it 'verifies debug_symbols method when xcode version is less than 12' do 192 | allow(Fastlane::Helper).to receive(:xcode_at_least?).and_return(false) 193 | result = described_class.debug_symbols(index: 0, params: {}) 194 | expect(result).to eq('') 195 | end 196 | 197 | it 'verifies debug_symbols method when :include_debug_symbols option is equal to false' do 198 | allow(Fastlane::Helper).to receive(:xcode_at_least?).and_return(true) 199 | params = { include_debug_symbols: false } 200 | result = described_class.debug_symbols(index: 0, params: params) 201 | expect(result).to eq('') 202 | end 203 | 204 | it 'verifies debug_symbols method' do 205 | test_data = 'testme' 206 | allow(nil).to receive(:xcarchive_BCSymbolMaps_path).and_return(test_data) 207 | allow(nil).to receive(:xcarchive_dSYMs_path).and_return(test_data) 208 | allow(nil).to receive(:framework).and_return(test_data) 209 | allow(File).to receive(:expand_path).and_return(test_data) 210 | allow(Dir).to receive(:exist?).and_return(true) 211 | allow(Dir).to receive(:children).and_return([test_data]) 212 | allow(Fastlane::Helper).to receive(:xcode_at_least?).and_return(true) 213 | params = { include_debug_symbols: true } 214 | result = described_class.debug_symbols(index: 0, params: params) 215 | expected_result = "-debug-symbols #{test_data}/#{test_data}.dSYM -debug-symbols #{test_data}" 216 | expect(result).to eq(expected_result) 217 | end 218 | 219 | it 'verifies debug_symbols method when :include_debug_symbols option was not provided' do 220 | test_data = 'testme' 221 | allow(nil).to receive(:xcarchive_BCSymbolMaps_path).and_return(test_data) 222 | allow(nil).to receive(:xcarchive_dSYMs_path).and_return(test_data) 223 | allow(nil).to receive(:framework).and_return(test_data) 224 | allow(File).to receive(:expand_path).and_return(test_data) 225 | allow(Dir).to receive(:exist?).and_return(true) 226 | allow(Dir).to receive(:children).and_return([test_data]) 227 | allow(Fastlane::Helper).to receive(:xcode_at_least?).and_return(true) 228 | result = described_class.debug_symbols(index: 0, params: {}) 229 | expected_result = "-debug-symbols #{test_data}/#{test_data}.dSYM -debug-symbols #{test_data}" 230 | expect(result).to eq(expected_result) 231 | end 232 | 233 | it 'verifies debug_symbols method when :include_dSYMs option is equals to false' do 234 | test_data = 'testme' 235 | allow(nil).to receive(:xcarchive_BCSymbolMaps_path).and_return(test_data) 236 | allow(nil).to receive(:xcarchive_dSYMs_path).and_return(test_data) 237 | allow(nil).to receive(:framework).and_return(test_data) 238 | allow(File).to receive(:expand_path).and_return(test_data) 239 | allow(Dir).to receive(:exist?).and_return(true) 240 | allow(Dir).to receive(:children).and_return([test_data]) 241 | allow(Fastlane::Helper).to receive(:xcode_at_least?).and_return(true) 242 | params = { include_debug_symbols: true, include_dSYMs: false } 243 | result = described_class.debug_symbols(index: 0, params: params) 244 | expected_result = "-debug-symbols #{test_data}" 245 | expect(result).to eq(expected_result) 246 | end 247 | 248 | it 'verifies debug_symbols method when :include_BCSymbolMaps and :enable_bitcode options are equal to false' do 249 | test_data = 'testme' 250 | allow(nil).to receive(:xcarchive_BCSymbolMaps_path).and_return(test_data) 251 | allow(nil).to receive(:xcarchive_dSYMs_path).and_return(test_data) 252 | allow(nil).to receive(:framework).and_return(test_data) 253 | allow(File).to receive(:expand_path).and_return(test_data) 254 | allow(Dir).to receive(:exist?).and_return(true) 255 | allow(Dir).to receive(:children).and_return([test_data]) 256 | allow(Fastlane::Helper).to receive(:xcode_at_least?).and_return(true) 257 | params = { include_debug_symbols: true, enable_bitcode: false, include_BCSymbolMaps: false } 258 | result = described_class.debug_symbols(index: 0, params: params) 259 | expected_result = "-debug-symbols #{test_data}/#{test_data}.dSYM" 260 | expect(result).to eq(expected_result) 261 | end 262 | 263 | it 'verifies create_xcframework method' do 264 | xcframework = 'xcframework' 265 | xcarchive = 'xcarchive' 266 | allow(nil).to receive(:xcframework_path).and_return(xcframework) 267 | allow(nil).to receive(:xcarchive_framework_path).and_return(xcarchive) 268 | params = { include_debug_symbols: false, destinations: destinations } 269 | result = described_class.create_xcframework(params) 270 | expected_result = 'set -o pipefail && xcodebuild -create-xcframework -framework ' \ 271 | "#{xcarchive} -framework #{xcarchive} -output #{xcframework}" 272 | expect(result).to eq(expected_result) 273 | end 274 | 275 | it 'verifies create_xcframework method when :allow_internal_distribution option is equal to true' do 276 | xcframework = 'xcframework' 277 | xcarchive = 'xcarchive' 278 | allow(nil).to receive(:xcframework_path).and_return(xcframework) 279 | allow(nil).to receive(:xcarchive_framework_path).and_return(xcarchive) 280 | params = { 281 | destinations: destinations, 282 | include_debug_symbols: false, 283 | allow_internal_distribution: true 284 | } 285 | result = described_class.create_xcframework(params) 286 | expected_result = 'set -o pipefail && xcodebuild -create-xcframework -allow-internal-distribution ' \ 287 | "-framework #{xcarchive} -framework #{xcarchive} -output #{xcframework}" 288 | expect(result).to eq(expected_result) 289 | end 290 | end 291 | end 292 | end 293 | -------------------------------------------------------------------------------- /spec/fastlane_create_xcframework_helper_spec.rb: -------------------------------------------------------------------------------- 1 | describe Fastlane do 2 | describe Fastlane::Helper::CreateXcframeworkHelper do 3 | describe 'Helper Test Suite' do 4 | let(:product_name) { 'iOSModule' } 5 | let(:framework_index) { 0 } 6 | let(:xc_output) { 'test' } 7 | 8 | it 'verifies product_name method' do 9 | params = { product_name: product_name } 10 | result = described_class.new(params).product_name 11 | expect(result).to eq(product_name) 12 | end 13 | 14 | it 'verifies that scheme can be used instead of the product_name' do 15 | params = { scheme: product_name } 16 | result = described_class.new(params).product_name 17 | expect(result).to eq(product_name) 18 | end 19 | 20 | it 'verifies xcframework method' do 21 | params = { scheme: product_name } 22 | result = described_class.new(params).xcframework 23 | expect(result).to eq("#{product_name}.xcframework") 24 | end 25 | 26 | it 'verifies framework method' do 27 | params = { scheme: product_name } 28 | result = described_class.new(params).framework 29 | expect(result).to eq("#{product_name}.framework") 30 | end 31 | 32 | it 'verifies xcframework_output_directory' do 33 | params = { xcframework_output_directory: xc_output } 34 | result = described_class.new(params).output_directory 35 | expect(result).to eq(xc_output) 36 | end 37 | 38 | it 'verifies empty xcframework_output_directory' do 39 | params = { xcframework_output_directory: '' } 40 | result = described_class.new(params).output_directory 41 | expect(result).to eq('') 42 | end 43 | 44 | it 'verifies xcarchive_path method' do 45 | params = { scheme: product_name, xcframework_output_directory: xc_output } 46 | result = described_class.new(params).xcarchive_path 47 | expect(result).to eq("#{xc_output}/archives") 48 | end 49 | 50 | it 'verifies xcarchive_path_for_destination method' do 51 | params = { scheme: product_name } 52 | helper = described_class.new(params) 53 | xcarchive_path = helper.xcarchive_path 54 | result = helper.xcarchive_path_for_destination(framework_index) 55 | expect(result).to eq("#{xcarchive_path}/#{framework_index}_#{product_name}.xcarchive") 56 | end 57 | 58 | it 'verifies xcarchive_framework_path method' do 59 | params = { scheme: product_name } 60 | helper = described_class.new(params) 61 | framework = helper.framework 62 | xcarchive_path_for_destination = helper.xcarchive_path_for_destination(framework_index) 63 | framework_path = "#{xcarchive_path_for_destination}/Products/Library/Frameworks/#{framework}" 64 | 65 | allow(File).to receive(:exist?).with(framework_path).and_return(true) 66 | result = helper.xcarchive_framework_path(framework_index) 67 | expect(result).to eq(framework_path) 68 | end 69 | 70 | it 'verifies xcarchive_framework_path method error message' do 71 | err = "▸ PRODUCT_NAME was misdefined: `#{product_name}`. Please, provide :product_name option" 72 | params = { scheme: product_name } 73 | expect do 74 | described_class.new(params).xcarchive_framework_path(framework_index) 75 | end.to raise_error(err) 76 | end 77 | 78 | it 'verifies xcarchive_frameworks_path method' do 79 | params = { scheme: product_name, destinations: ['iOS', 'macOS'] } 80 | helper = described_class.new(params) 81 | framework = helper.framework 82 | 83 | expected_result = params[:destinations].each_with_index.map do |_, i| 84 | xcarchive_path_for_destination = helper.xcarchive_path_for_destination(i) 85 | framework_path = "#{xcarchive_path_for_destination}/Products/Library/Frameworks/#{framework}" 86 | allow(File).to receive(:exist?).with(framework_path).and_return(true) 87 | helper.xcarchive_framework_path(i) 88 | end 89 | 90 | result = helper.xcarchive_frameworks_path 91 | expect(result).to eq(expected_result) 92 | end 93 | 94 | it 'verifies xcarchive_dSYMs_path method' do 95 | params = { scheme: product_name } 96 | helper = described_class.new(params) 97 | expected_result = "#{helper.xcarchive_path_for_destination(framework_index)}/dSYMS" 98 | allow(File).to receive(:expand_path).and_return(expected_result) 99 | result = helper.xcarchive_dSYMs_path(framework_index) 100 | expect(result).to eq(expected_result) 101 | end 102 | 103 | it 'verifies xcframework_dSYMs_path method' do 104 | params = { scheme: product_name, xcframework_output_directory: xc_output } 105 | helper = described_class.new(params) 106 | output_directory = helper.output_directory 107 | expected_result = "#{output_directory}/#{product_name}.dSYMs" 108 | allow(File).to receive(:expand_path).and_return(expected_result) 109 | result = helper.xcframework_dSYMs_path 110 | expect(result).to eq(expected_result) 111 | end 112 | 113 | it 'verifies xcarchive_BCSymbolMaps_path method' do 114 | params = { scheme: product_name } 115 | helper = described_class.new(params) 116 | expected_result = "#{helper.xcarchive_path_for_destination(framework_index)}/BCSymbolMaps" 117 | allow(File).to receive(:expand_path).and_return(expected_result) 118 | result = helper.xcarchive_BCSymbolMaps_path(framework_index) 119 | expect(result).to eq(expected_result) 120 | end 121 | 122 | it 'verifies xcframework_BCSymbolMaps_path method' do 123 | params = { scheme: product_name, xcframework_output_directory: xc_output } 124 | helper = described_class.new(params) 125 | output_directory = helper.output_directory 126 | expected_result = "#{output_directory}/#{product_name}.BCSymbolMaps" 127 | allow(File).to receive(:expand_path).and_return(expected_result) 128 | result = helper.xcframework_BCSymbolMaps_path 129 | expect(result).to eq(expected_result) 130 | end 131 | 132 | it 'verifies xcframework_path method' do 133 | params = { scheme: product_name, xcframework_output_directory: xc_output } 134 | helper = described_class.new(params) 135 | output_directory = helper.output_directory 136 | xcframework = helper.xcframework 137 | expected_result = "#{output_directory}/#{xcframework}" 138 | allow(File).to receive(:expand_path).and_return(expected_result) 139 | result = helper.xcframework_path 140 | expect(result).to eq(expected_result) 141 | end 142 | 143 | it 'verifies library_identifier method' do 144 | params = { scheme: product_name, xcframework_output_directory: xc_output } 145 | expected_result = 'path/to/library.framework' 146 | allow(Dir).to receive(:chdir).and_return([expected_result]) 147 | allow(FileUtils).to receive(:compare_file).and_return(true) 148 | allow(File).to receive(:exist?).and_return(true) 149 | result = described_class.new(params).library_identifier(framework_index) 150 | expect(result).to eq(expected_result) 151 | end 152 | 153 | it 'verifies library_identifier method error message' do 154 | allow(Dir).to receive(:chdir).and_return(['path/to/library.framework']) 155 | allow(FileUtils).to receive(:compare_file).and_return(false) 156 | allow(File).to receive(:exist?).and_return(true) 157 | 158 | params = { scheme: product_name, xcframework_output_directory: xc_output } 159 | helper = described_class.new(params) 160 | xcframework_path = helper.xcframework_path 161 | framework_path = helper.xcarchive_framework_path(framework_index) 162 | err = "Error: #{xcframework_path} doesn't contain #{framework_path}" 163 | expect { helper.library_identifier(framework_index) }.to raise_error(err) 164 | end 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) 2 | 3 | require 'simplecov' 4 | 5 | # SimpleCov.minimum_coverage 95 6 | SimpleCov.start 7 | 8 | # Allow message expectations on nil 9 | RSpec.configure do |config| 10 | config.mock_with(:rspec) do |mocks| 11 | mocks.allow_message_expectations_on_nil = true 12 | end 13 | end 14 | 15 | # This module is only used to check the environment is currently a testing env 16 | module SpecHelper 17 | end 18 | 19 | require 'fastlane' # to import the Action super class 20 | require 'fastlane/plugin/create_xcframework' # import the actual plugin 21 | 22 | Fastlane.load_actions # load other actions (in case your plugin calls other actions or shared values) 23 | --------------------------------------------------------------------------------