class Xcodegen::XcodeprojWriter

Constants

CONFIG_PROFILE_PATH
PRODUCT_TYPE_UTI_INV

Sourced from Cocoapods:Xcodeproj project. This should be kept up to date with that gem.

TARGET_CONFIG_PROFILE_PATH

Public Class Methods

add_target(target, project, target_refs, spec_configuration_type_map) click to toggle source

@param target [Xcodegen::Specfile::Target] @param project [Xcodeproj::Project] @param target_refs [Hash<String, Xcodeproj::PBXNativeTarget>] @param spec_configuration_type_map [Hash<String, String>] @return [Xcodeproj::PBXNativeTarget]

# File lib/xcodeproj/xcodeproj_writer.rb, line 100
def self.add_target(target, project, target_refs, spec_configuration_type_map)
        requested_target_refs = target.references.select { |ref| ref.is_a? Xcodegen::Specfile::Target::TargetReference }
        target_references = requested_target_refs.map { |ref|
                # If the referenced target has not been added to the project yet, skip this target for now
                unless target_refs.has_key? ref.target_name
                        return nil
                end

                next target_refs[ref.target_name]
        }.compact

        if requested_target_refs.length != target_references.length
                puts Paint["Warning: Not all target references could be resolved for target: '#{target.name}'.", :yellow]
        end

        target_build_settings = {}
        target.configurations.each { |config|
                build_settings = {}
                config.profiles.map { |profile_name|
                        [profile_name, File.join(TARGET_CONFIG_PROFILE_PATH, "#{profile_name.sub(':', '_')}.yml")]
                }.map { |data|
                        profile_name, profile_file_name = data
                        unless File.exist? profile_file_name
                                puts Paint["Warning: unrecognised project configuration profile '#{profile_name}'. Ignoring...", :yellow]
                                next nil
                        end

                        next YAML.load_file(profile_file_name)
                }.each { |profile_data|
                        build_settings = build_settings.merge (profile_data || {})
                }

                build_settings = build_settings.merge config.settings
                target_build_settings[config.name] = { :type => spec_configuration_type_map[config.name], :settings => build_settings }
        }

        sdk = target_build_settings[target_build_settings.keys.first][:settings]['SDKROOT']

        unless sdk != nil
                puts Paint["Warning: SDKROOT not found in configuration for target: '#{target.name}'. Ignoring...", :yellow]
                return nil
        end

        if sdk.include? 'iphoneos'
                platform = :ios
        elsif sdk.include? 'macosx'
                platform = :osx
        elsif sdk.include? 'appletvos'
                platform = :tvos
        elsif sdk.include? 'watchos'
                platform = :watchos
        else
                puts Paint["Warning: SDKROOT #{build_settings['SDKROOT']} not recognised in configuration for target: '#{target.name}'. Ignoring...", :yellow]
                return nil
        end

        native_target = project.new_target PRODUCT_TYPE_UTI_INV[target.type], target.name, platform, nil, nil, :swift
        native_target.build_configurations.clear
        target_build_settings.each { |name, data|
                config = native_target.add_build_configuration(name, data[:type])
                config.build_settings = data[:settings]
        }

        target_references.each { |native_ref|
                native_target.add_dependency native_ref
        }

        requested_sys_framework_refs = target.references
                .select { |ref| ref.is_a? Xcodegen::Specfile::Target::SystemFrameworkReference }
                .map { |ref| ref.name }
                .select { |ref| ref != 'Foundation' } # Filter out Foundation as it's already added by default
        native_target.add_system_framework requested_sys_framework_refs

        requested_sys_library_refs = target.references
                .select { |ref| ref.is_a? Xcodegen::Specfile::Target::SystemLibraryReference }
                .map { |ref| ref.name }
        native_target.add_system_library requested_sys_library_refs

        subproj_group = project.frameworks_group.groups.find { |g| g.display_name == '$subproj' }
        if subproj_group == nil
                 subproj_group = project.frameworks_group.new_group '$subproj', nil, '<group>'
        end
        framework_group = project.frameworks_group.groups.find { |group| group.display_name == '$local' }
        if framework_group == nil
                framework_group = project.frameworks_group.new_group '$local', nil, '<group>'
        end
        target.references.select { |ref| ref.is_a? Xcodegen::Specfile::Target::FrameworkReference }.each { |f|
                subproj = subproj_group.new_file f.project_path
                remote_project = Xcodeproj::Project.open f.project_path

                f.settings['frameworks'].each { |f_opts|
                        remote_target = remote_project.targets.select { |t|
                                t.product_reference.path == f_opts['name'] and t.product_type === 'com.apple.product-type.framework' and [nil, platform].include? t.platform_name
                        }.first
                        next if remote_target == nil

                        framework = subproj.file_reference_proxies.select { |p| p.path == remote_target.product_reference.path }.first

                        framework_path = File.expand_path framework.path, File.dirname(f.project_path)
                        framework_group.new_file framework_path

                        native_target.add_dependency remote_target

                        if f_opts['copy']
                                embed_phase = project.new(Xcodeproj::Project::Object::PBXCopyFilesBuildPhase)
                                embed_phase.name = "Embed Framework #{framework.path}"
                                embed_phase.symbol_dst_subfolder_spec = :frameworks
                                native_target.build_phases.insert(native_target.build_phases.count, embed_phase)

                                attributes = ['RemoveHeadersOnCopy']

                                if f_opts['codeSignOnCopy']
                                        attributes << 'CodeSignOnCopy'
                                end

                                framework_build_file = embed_phase.add_file_reference framework
                                framework_build_file.settings = { 'ATTRIBUTES' => attributes }
                        end

                        native_target.frameworks_build_phase.add_file_reference framework
                }
        }

        return native_target
end
create_group(parent_group, components) click to toggle source
# File lib/xcodeproj/xcodeproj_writer.rb, line 226
def self.create_group(parent_group, components)
        if components.first == nil
                return parent_group
        end
        group = parent_group[components.first]
        unless group
                group = parent_group.new_group(components.first)
                group.source_tree = '<group>'
                group.path = components.first
        end
        create_group group, components.drop(1)
end
write(source_spec, destination) click to toggle source

@param spec [Xcodegen::Specfile] @param destination [String]

# File lib/xcodeproj/xcodeproj_writer.rb, line 36
def self.write(source_spec, destination)
        # Create a clone of the spec to avoid affecting the original referenced object
        # noinspection RubyResolve
        spec = Marshal.load(Marshal.dump(source_spec))

        unless spec != nil and spec.is_a? Xcodegen::Specfile
                raise StandardError.new 'Invalid spec file'
        end

        unless spec.configurations.length > 0
                raise StandardError.new 'Spec must have at least one configuration'
        end

        if spec.variants.count == 0
                write_xcodeproj spec, File.join(destination, 'project.xcodeproj')
                puts Paint['Generated project.xcodeproj', :green]
        else
                # Generate a derived spec for each variant and write out the variants
                specs = spec.variants.map { |variant|
                        next nil if variant.abstract

                        variant_targets = DeepClone.clone variant.targets
                        spec_targets = DeepClone.clone spec.targets

                        variant_targets.each { |target|
                                spec_target = spec_targets.find { |st| st.name == target.name }
                                next if spec_target == nil

                                spec_target.source_dir = spec_target.source_dir.unshift(*target.source_dir).uniq
                                spec_target.res_dir = spec_target.res_dir.unshift(*target.res_dir).uniq
                                spec_target.file_excludes = [].unshift(*spec_target.file_excludes).unshift(*target.file_excludes).uniq

                                target.configurations.each { |configuration|
                                        spec_config = spec_target.configurations.find { |sc| sc.name == configuration.name }
                                        spec_config.settings.merge! configuration.settings
                                        spec_config.profiles = [].unshift(*configuration.profiles).unshift(*spec_config.profiles).uniq
                                }

                                spec_target.file_excludes = [].unshift(*spec_target.file_excludes).unshift(*target.file_excludes)
                                spec_target.options = [].unshift(*spec_target.options).unshift(*target.options)
                                spec_target.references = [].unshift(*spec_target.references).unshift(*target.references)
                        }

                        [variant.name, Xcodegen::Specfile.new(spec.version, spec_targets, spec.configurations, [], spec.base_dir)]
                }.compact.to_h

                specs.each { |name, variant_spec|
                        if name == '$base'
                                write_xcodeproj variant_spec, File.join(destination, 'project.xcodeproj')
                                puts Paint['Generated project.xcodeproj', :green]
                        else
                                project_name = name.downcase.gsub(/[^0-9a-z]/, '')
                                write_xcodeproj variant_spec, File.join(destination, "#{project_name}.xcodeproj")
                                puts Paint["Generated #{project_name}.xcodeproj", :green]
                        end
                }
        end
end

Private Class Methods

add_files_to_target(target, native_target, project, project_directory) click to toggle source

@param target [Xcodegen::Specfile::Target] @param native_target [Xcodeproj::PBXNativeTarget] @param project [Xcodeproj::Project] @param project_working_dir [String]

# File lib/xcodeproj/xcodeproj_writer.rb, line 327
def self.add_files_to_target(target, native_target, project, project_directory)
        all_source_files = []
        grouped_source_files = {}
        source_files_minus_dir = []

        target.source_dir.each { |source_dir|
                # For some reason our symlink-traversing glob duplicates the results, so we use .uniq to fix that
                new_files = Dir.glob("#{source_dir}/**{,/*/**}/*").select { |file|
                        !(file.include? '.xcassets/') and
                                !(file.include? '.bundle/') and
                                !(file.end_with? 'Info.plist') and
                                !(file.include? '.lproj') and
                                (file.include? '.')
                }.uniq.select { |f|
                        source_files_minus_dir.count(f.sub(source_dir, '')) == 0
                }

                new_files_minus_dir = new_files.map { |f| f.sub(source_dir, '') }
                all_source_files.unshift(*new_files)
                grouped_source_files[source_dir] = new_files
                source_files_minus_dir = source_files_minus_dir.unshift(*new_files_minus_dir).uniq
        }

        grouped_source_files.each { |source_dir, all_files|
                files = all_files.select { |file|
                        !(target.file_excludes.any? { |exclude|
                                File.fnmatch(exclude, file)
                        })
                }

                files = files.select { |file|
                        if File.directory?(file) and !file.end_with? '.xcassets' and !file.end_with? '.xcdatamodeld' and !file.end_with? '.bundle'
                                next false
                        end

                        next !(file.include? '.framework/') && !(file.include? '.xcdatamodeld/') && !(file.include? '.bundle/')
                }

                rel_source_root = source_dir.sub(project_directory, '')
                if rel_source_root.start_with? '/'
                        rel_source_root[0] = ''
                end

                source_group = project.new_group(File.basename(source_dir), rel_source_root, 'SOURCE_ROOT')

                files.map { |file|
                        new_file = file.sub(source_dir, '')
                        if new_file.start_with? '/'
                                new_file[0] = ''
                        end
                        next new_file
                }.each { |file|
                        native_group = file.include?('/') ? create_group(source_group, File.dirname(file).split('/')) : source_group
                        native_file = native_group.new_file File.basename(file)
                        if file.end_with? '.swift' or file.end_with? '.m' or file.end_with? '.mm'
                                native_target.source_build_phase.files_references << native_file
                                native_target.add_file_references [native_file]
                        elsif file.end_with? '.entitlements'
                                next
                        elsif file.include? '.' # Files without an extension break Xcode compilation during resource phase
                                native_target.add_resources [native_file]
                        end
                }
        }

        if target.res_dir.count > 0
                target.res_dir.select { |res_dir|
                        lfiles = Dir.glob(File.join(res_dir, '*.lproj', '**', '*'))
                        if lfiles.length > 0
                                resource_group = project.groups.find { |group| group.display_name == "$lang:#{target.name}" }
                                if resource_group == nil
                                        resource_group = project.new_group("$lang:#{target.name}", nil, '<group>')
                                        resource_group.source_tree = 'SOURCE_ROOT'
                                end

                                # Create a virtual path since lproj files go through a layer of indirection before hitting the filesystem
                                lproj_variant_files = []
                                lfiles.map { |lfile|
                                        new_lfile = lfile.sub(res_dir, '')
                                        if new_lfile.start_with? '/'
                                                new_lfile[0] = ''
                                        end
                                        next new_lfile
                                }.each { |lfile|
                                        lfile_components = lfile.split('/')
                                        lfile_lproj_idx = lfile_components.index{|component|
                                                component.include? '.lproj'
                                        }

                                        lfile_variant_components = []
                                        lfile_variant_components.unshift *lfile_components
                                        lfile_variant_components.shift(lfile_lproj_idx + 1)
                                        lfile_variant_path = lfile_variant_components.join('/')
                                        unless lproj_variant_files.include? lfile_variant_path
                                                lproj_variant_files << lfile_variant_path
                                        end
                                }

                                lproj_variant_files.each { |lproj_file|
                                        variant_group = resource_group.new_variant_group(lproj_file, res_dir, '<group>')
                                        # Add all lproj files to the variant group

                                        Dir.glob(File.join(res_dir, '*.lproj', lproj_file)).each { |file|
                                                native_file = variant_group.new_file(file, '<group>')
                                                native_target.add_resources [native_file]
                                        }
                                }
                        end
                }
        end

        framework_files = all_source_files.select { |file|
                file.end_with? '.framework'
        }

        framework_group = project.frameworks_group.groups.find { |group| group.display_name == '$local' }
        if framework_group == nil
                framework_group = project.frameworks_group.new_group '$local', nil, '<group>'
        end
        # The 'Embed Frameworks' phase is missing by default from the Xcodeproj template, so we have to add it.
        embed_phase = project.new(Xcodeproj::Project::Object::PBXCopyFilesBuildPhase)
        embed_phase.name = 'Embed Frameworks'
        embed_phase.symbol_dst_subfolder_spec = :frameworks
        native_target.build_phases.insert(native_target.build_phases.count, embed_phase)

        framework_files.map { |framework|
                framework_group.new_file framework
        }.each { |framework|
                (embed_phase.add_file_reference framework).settings = { 'ATTRIBUTES' => ['CodeSignOnCopy', 'RemoveHeadersOnCopy'] }
                native_target.frameworks_build_phase.add_file_reference framework
        }

        target.references.select { |ref| ref.is_a? Xcodegen::Specfile::Target::LocalFrameworkReference }.each { |ref|
                framework = framework_group.new_file ref.framework_path

                # Link
                native_target.frameworks_build_phase.add_file_reference framework

                # Embed
                settings = ref.settings || {}
                unless settings.has_key?('copy') and settings['copy'] == false
                        attributes = ['RemoveHeadersOnCopy']

                        unless settings.has_key?('codeSignOnCopy') and settings['codeSignOnCopy'] == false
                                attributes.push 'CodeSignOnCopy'
                        end

                        (embed_phase.add_file_reference framework).settings = { 'ATTRIBUTES' => attributes }
                end
        }

        target.run_scripts.each { |script|
                script_name = script.script_path
                script = File.read(File.join(project_directory, script.script_path))

                script_phase = native_target.new_shell_script_build_phase script_name
                script_phase.shell_script = script
        }
end
write_xcodeproj(spec, filename) click to toggle source
# File lib/xcodeproj/xcodeproj_writer.rb, line 240
def self.write_xcodeproj(spec, filename)
        spec_xcodeproj_type_map = {}
        spec_xcodeproj_type_map['debug'] = :debug
        spec_xcodeproj_type_map['release'] = :release
        spec_configuration_type_map = {}

        # Create the new project file and clear out any defaults we don't need
        project = Xcodeproj::Project.new(filename)
        project.build_configurations.clear

        # Create all of the project-level configurations
        spec.configurations.each { |spec_config|
                config = project.add_build_configuration spec_config.name, spec_xcodeproj_type_map[spec_config.type]
                spec_configuration_type_map[spec_config.name] = spec_config.type
                build_settings = {}

                spec_config.profiles.map { |profile_name|
                        [profile_name, File.join(CONFIG_PROFILE_PATH, "#{profile_name.sub(':', '_')}.yml")]
                }.map { |data|
                        profile_name, profile_file_name = data
                        unless File.exist? profile_file_name
                                puts Paint["Warning: unrecognised project configuration profile '#{profile_name}'. Ignoring...", :yellow]
                                next nil
                        end

                        next YAML.load_file(profile_file_name)
                }.each { |profile_data|
                        build_settings = build_settings.merge (profile_data || {})
                }

                build_settings = build_settings.merge spec_config.overrides
                config.build_settings = build_settings
        }

        # Update build configuration list's defaultConfigurationName to be the first configuration in our spec
        project.build_configuration_list.default_configuration_name = spec.configurations[0].name

        # Create all of the targets
        target_refs = {}
        # noinspection RubyResolve
        remaining_targets = Marshal.load(Marshal.dump(spec.targets))
        iterations_remaining = remaining_targets.count
        remaining_targets_removed = 0

        # As we don't know which unreferenced targets are where, attempt to create each target in turn
        # If a target cannot be created due to its reference not existing in target_refs, it will be skipped
        # until the next cycle.
        #
        # If an entire cycle passes without an element being removed from remaining_targets, it is assumed we
        # are encountering a circular reference, and in that scenario we break early.
        while remaining_targets.length > 0
                target = remaining_targets.first
                if target == nil
                        break
                end

                native_target = add_target target, project, target_refs, spec_configuration_type_map
                if native_target != nil
                        target_refs[target.name] = native_target
                        remaining_targets_removed = remaining_targets_removed + 1
                        remaining_targets.shift
                end

                iterations_remaining = iterations_remaining - 1
                if iterations_remaining == 0
                        if remaining_targets_removed == 0
                                raise StandardError.new 'Circular target references were found in spec, aborting'
                        else
                                iterations_remaining = remaining_targets.length
                                remaining_targets_removed = 0
                        end
                end
                remaining_targets.rotate!
        end

        spec.targets.each { |target|
                add_files_to_target target, target_refs[target.name], project, spec.base_dir
        }

        project.save filename
        return nil
end