Skip to content

Support external target dependencies via subprojects#701

Merged
yonaskolb merged 14 commits intoyonaskolb:masterfrom
evandcoleman:edc-external-target-dependency
Feb 1, 2020
Merged

Support external target dependencies via subprojects#701
yonaskolb merged 14 commits intoyonaskolb:masterfrom
evandcoleman:edc-external-target-dependency

Conversation

@evandcoleman
Copy link
Copy Markdown
Contributor

@evandcoleman evandcoleman commented Oct 30, 2019

Resolves #570

This builds upon the work done in #655 by allowing target dependencies to reference targets in other Xcode projects.

Motivation

At my company, we have several internal dependencies that we include via submodules. We'd like the ability to include these dependencies as subprojects and reference their targets in our main Xcode project.

Changes

As introduced in #655, subprojects are declared at the top level.

projectReferences:
  FooLib:
    path: path/to/FooLib.xcodeproj
targets:
  MyApp:
    type: application
    platform: iOS
    deploymentTarget: "11.0"
    sources: [Sources]
    dependencies:
      - target: FooLib/MyTarget

@evandcoleman evandcoleman force-pushed the edc-external-target-dependency branch from 08f7443 to 253cbab Compare November 1, 2019 14:42
@liamnichols
Copy link
Copy Markdown
Contributor

Just wanted to say thanks for this by the way! I'm experimenting with adopting XcodeGen and this would have been one of the blockers for us however when running your fork it works great, so good timing 😄

I did however noticed something while testing... But I'm not sure if it relates to this change in particular or if it was to do with the initial introduction of projectReferences... I'm gonna go ahead and ask anyway...

If I declare my projectReferences from within an included spec in a different directory, the path isn't relative to the directory that the included spec was contained in and instead its relative to the main spec. For example:

ProjectDefinitions/base.yml

---
projectReferences:
  Network:
    path: Frameworks/Network/Network.xcodeproj
  Logger:
    path: Frameworks/Logger/Logger.xcodeproj
  Persistence:
    path: Frameworks/Persistence/Persistence.xcodeproj
options:
  xcodeVersion: "11.1"
  deploymentTarget:
    iOS: "11.4"
settings:
  base:
    CURRENT_PROJECT_VERSION: 9.58.0.0
    VERSIONING_SYSTEM: apple-generic
    SWIFT_VERSION: 5.0
    INFOPLIST_FILE: ${TARGET_NAME}/SupportingFiles/Info.plist
targetTemplates:
  CoreModule:
    platform: iOS
    type: framework
    sources:
      - ../Frameworks/${frameworkName}/${frameworkName}

project.yml

name: Global
include:
  - ProjectDefinitions/base.yml
...

This example produces the xcodeproj correctly however the ProjectDefinitions/base.yml looks wrong since I have my projectReferences paths relative to a different directory from the targetTemplates sources path.

I first tried implementing the projectReferences paths relative to the file they're defined in but I was then hit with the following while generating the project:

⚙️  Generating project...
The project cannot be found at ../Frameworks/Network/Network.xcodeproj

I just wanted to flag it here incase it's related to this change specifically... If it's not i can rase it separately and even have a go at correcting it. Thanks!

@yonaskolb
Copy link
Copy Markdown
Owner

Thank you for the great PR @evandcoleman! You've correctly identified the planned next step in harnessing the new project references 👍
I don't have time to review this right now, but will do so in the next couple of days. As an overview things looks pretty good, there's just some code duplication we can remove

@evandcoleman
Copy link
Copy Markdown
Contributor Author

evandcoleman commented Nov 7, 2019

@yonaskolb Thanks! I'm really looking forward to using XcodeGen and this is the last step for us! I'd also like your opinion on @liamnichols' comment. I wasn't really sure if I should be referencing subprojects via their relative path, or to prepend the project basepath so I think there's some room for improvement and consistency there.

@acecilia
Copy link
Copy Markdown
Collaborator

just adding some more context here. I managed to depend on targets from external projects without using the new projectReferences option by:

  1. Adding the external project to the main one using the fileGroups option.
  2. Using implicit framework dependencies.

An example project.yml:

name: Example

options:
  minimumXcodeGenVersion: 2.10.1

fileGroups: [
  FooLib/FooLib.xcodeproj
]

targets:
  Bar:
    platform: iOS
    type: application
    dependencies:
      - framework: FooLib.framework
        implicit: true

Find a working example here (under the Workaround folder): https://github.com/acecilia/XcodeGenProjectReference

@evandcoleman
Copy link
Copy Markdown
Contributor Author

@acecilia I don't believe that will actually add a target dependency to your target. That was one of the driving factors here since there's no requirement that target product names be unique.

@acecilia
Copy link
Copy Markdown
Collaborator

@evandcoleman It adds a framework dependency that Xcode is able to implicitly understand that corresponds to the target in the external project (due to the "Find Implicit Dependencies" option of the scheme), and thus it will build both correctly. If you do not believe it works you can have a look at the example project.

That said, it does not turn this PR invalid. My goal was just to add my experience and some extra context to this discussion :)

@evandcoleman
Copy link
Copy Markdown
Contributor Author

@yonaskolb Is this PR still being considered? The solutions posted here may work for some, but we include several dependencies that have multiple targets with the same product name (for different platforms). So implicit dependencies will not work.

@mathieutozer
Copy link
Copy Markdown

mathieutozer commented Jan 11, 2020

I'm wrestling with a similar issue. Unfortunately some of my sub dependencies have cocoapod dependencies too so the fileGroups workaround that @acecilia demonstrated doesn't work. Some of them support Carthage so I could move to that and dump cocoa pods for everything below my root applications, unless anyone can think of another work around. I understand trying to marry the cocoa pods approach and this one is not really feasible or desirable.

For context I am trying to modularize a codebase into a tree of modules that compile into cross platform apps.

Copy link
Copy Markdown
Owner

@yonaskolb yonaskolb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late review @evandcoleman. Great work on this one! I've left some comments above. This PR will need to have master merged in as well (preferable to a rebase at this point, so we have visibility of the changes, in regards to conflict resolution)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we pull common stuff like this out and above the switch

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is adding all projects into a Projects group sufficient for most people? I could imagine people wanting to add their projects to a specific group.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my use case, it doesn't really matter. But I agree, I could see a use for it. How would you think that would work best? A new parameter under projectReferences?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now it's ok 👍

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is getting too large and complicated. Will have to look at splitting this up into things like TargetGenerator and the like, after this

@yonaskolb
Copy link
Copy Markdown
Owner

@liamnichols the relative project reference paths in included files should be fixed with #740 and #751

@evandcoleman evandcoleman force-pushed the edc-external-target-dependency branch from 453f5f2 to 75b426d Compare January 13, 2020 17:12
@evandcoleman
Copy link
Copy Markdown
Contributor Author

@yonaskolb Just pushed up a bunch of changes and rebased with master. Let me know what you think of the dependency target generation now. I opted to create a "fake" Target using the subproject dependency's PBXTarget. This allowed me to reuse almost all of the generation code for both local and external targets.

Copy link
Copy Markdown
Owner

@yonaskolb yonaskolb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating @evandcoleman. Left some comments above

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right about not being able to import GenerationError. We can just leave this as it is for now

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now it's ok 👍

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there supposed to be 2 build files added? As these are referencing the same PBXFileReference, XcodeProj can't give them a deterministic id hence the failing diff tests.
If they both need to be there then XcodeProj needs to be updated to take into account something that is different about them like the settings. That has to be done here https://github.com/tuist/XcodeProj/blob/master/Sources/XcodeProj/Utils/ReferenceGenerator.swift#L290

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, one is for linking and the other is to embed it. I notice this is also the case for Framework2.framework in that test project, but it does not cause a failing diff test. Any idea why?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be fixed in tuist/XcodeProj#518. Will update this PR once that is merged.

Copy link
Copy Markdown
Owner

@yonaskolb yonaskolb Jan 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same logic as above. Let's combine to an extension on PBXProductType in this file

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just use the following once defaultLinkage is extracted to PBXProductType in Linkage.swift:

let dependencyLinkage = dependencyTarget.productType.defaultLinkage ?? .none

@evandcoleman evandcoleman force-pushed the edc-external-target-dependency branch from ab0cc74 to 8cb1f5a Compare January 29, 2020 16:37
@evandcoleman
Copy link
Copy Markdown
Contributor Author

@yonaskolb Updated to the latest XcodeProj and tests are passing now 🎉

@mobileben
Copy link
Copy Markdown

mobileben commented Feb 1, 2020

I've started to look at projectReferences. Our use case is to dynamically to add sub-projects versus prebuilt binaries for debugging. I'm experimenting with this. What I have found though is a couple of issues. Not sure if this latest merge will address.

  1. When you include a project reference, it puts it in a folder group based on path. I ideally want my sub-projects at the root so they are easier to see. I would imagine this is something others would want as well, because it makes it easier to work with subprojects.
  2. I added a scheme which would also be the scheme for the main target which is the app. To this scheme I added the project reference target. This resulted in the creation of an identical schema (but a framework).

"Application"

name: Project1
options:
  bundleIdPrefix:  com.company.proj1
fileGroups:
  - ../proj2/Proj2Framework.xcodeproj
targets:
  Project1:
    type: application
    platform: iOS
    deploymentTarget: "11.0"
    sources:
      - Classes
projectReferences: 
  Proj2Framework:
    path: ../proj2/Proj2Framework.xcodeproj
schemes:
  Project1:
    build:
      targets:
        Proj2Framework/Project2: ["build"]

"Framework"

name: Proj2Framework
options:
  bundleIdPrefix: com.company.proj2
targets:
  Project2:
      type: framework
      platform: iOS
      deploymentTarget: "11.0"
      sources: Classes

Screen_Shot_2020-01-31_at_5_44_37_PM

I did use evandcoleman's branch here: https://github.com/evandcoleman/XcodeGen/tree/edc-external-target-dependency

The issue with the scheme exists still (could be user error of course). The difference is a group called Projects is created with the subproject. Definitely better, but would be helpful to be able to define where to place it.

Screen Shot 2020-01-31 at 5 55 06 PM

@evandcoleman
Copy link
Copy Markdown
Contributor Author

@mobileben I believe the issue you are seeing is not related to this PR. This PR relates to depending on targets from subprojects. The feature to create schemes based on external targets was done in #655.

Copy link
Copy Markdown
Owner

@yonaskolb yonaskolb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @evandcoleman!
I've just fixed some small conflicts and added a note in the docs

@yonaskolb yonaskolb merged commit 6bfd620 into yonaskolb:master Feb 1, 2020
@evandcoleman evandcoleman deleted the edc-external-target-dependency branch February 2, 2020 16:27
@evandcoleman
Copy link
Copy Markdown
Contributor Author

Thanks @yonaskolb!

@RayCyder
Copy link
Copy Markdown

RayCyder commented Mar 11, 2025

what i found is add subprojects target dependency is now not available , although this MR is merged. but with ruby and xcodeproj it is not so hard to realize it since we can use xcodegen add subproject without add target dependency. so here is the ruby to add target dependency after using xcodegen to add subproject:(AI generated:)
`#!/usr/bin/env ruby
require 'xcodeproj'

# Path to the main project and subprojects
MAIN_PROJECT_PATH = 'SoVpn.xcodeproj'
SHADOWSOCKS_PROJECT_PATH = 'deps/new/shadowsocks-libev/xcode/shadowsocks-libev.xcodeproj'
TUN2SOCKS_PROJECT_PATH = 'deps/new/tun2socks-iOS/xcode/BADVPN.xcodeproj'
\ # get subproject file reference
def get_subproject(main_project, subproject_path, target_name)
subproj_ref = main_project.root_object.project_references.find { |ref| ref[:project_ref].path == subproject_path }
unless subproj_ref
group = main_project.root_object.project_references.count == 0 ?
main_project.new_group('Dependencies') :
main_project.root_object.project_references[0][:project_ref].parent

proj_ref = group.new_file(subproject_path)
main_project.root_object.project_references << {
  :project_ref => proj_ref
}

else
puts "Subproject not exists#{subproject_path}"
end

# Open subproject
subproj = Xcodeproj::Project.open(subproject_path)
target = subproj.targets.find { |t| t.name == target_name }

raise "Could not find target #{target_name} in #{subproject_path}" unless target

[subproj, target]
end

begin
# Open main project
project = Xcodeproj::Project.open(MAIN_PROJECT_PATH)

# Get SoVPNDeps target
sovpn_deps_target = project.targets.find { |t| t.name == 'SoVPNDeps' }
raise "Could not find SoVPNDeps target" unless sovpn_deps_target

# Add shadowsocks project
ss_proj, ss_target = get_subproject(project, SHADOWSOCKS_PROJECT_PATH, 'shadowsocks-libev-shared')
# Add tun2socks project
tun_proj, tun_target = get_subproject(project, TUN2SOCKS_PROJECT_PATH, 'interface')

# Add dependencies to SoVPNDeps
#check if the target is already in the dependencies
if !sovpn_deps_target.dependencies.include?(ss_target)
sovpn_deps_target.add_dependency(ss_target)
end
if !sovpn_deps_target.dependencies.include?(tun_target)
sovpn_deps_target.add_dependency(tun_target)
end

# Save project
project.save
puts "Successfully modified project!"`

@RayCyder
Copy link
Copy Markdown

AI is not work, manually hard work for me :
#!/usr/bin/env ruby
require 'xcodeproj'

Path to the main project and subprojects

MAIN_PROJECT_PATH = 'SoVpn.xcodeproj'
SHADOWSOCKS_PROJECT_PATH = 'deps/new/shadowsocks-libev/xcode/shadowsocks-libev.xcodeproj'
TUN2SOCKS_PROJECT_PATH = 'deps/new/tun2socks-iOS/xcode/BADVPN.xcodeproj'

Add subprojects

def add_subproject(main_project, subproject_path)
# Check if project reference already exists
existing_ref = main_project.root_object.project_references.find { |ref| ref[:project_ref].path == subproject_path }
return existing_ref[:project_ref] if existing_ref

#here should use main_group,in new_reference should create a Products Group
reference = main_project.main_group.new_reference(subproject_path)
#puts "reference: #{reference.class}"
main_project.save()
return reference

end

def get_subproject(main_project, subproject_path, target_name)
# Add subproject file reference
subproj_ref = main_project.root_object.project_references.find { |ref| ref[:project_ref].path == subproject_path }
#puts subproj_ref
unless subproj_ref
puts "Subproject not exists:#{subproject_path} #{subproj_ref}"
end

# Open subproject
subproj = Xcodeproj::Project.open(subproject_path)
target = subproj.targets.find { |t| t.name == target_name }
raise "Could not find target #{target_name} in #{subproject_path}" unless target
[subproj, target]

end

def add_link_and_embeded(project,main_target,sub_target_ref)

# Get or create embed frameworks build phase (do this once outside the config loop) for embedding
embed_build_phase = main_target.build_phases.find { |bp| bp.display_name == 'Embed Frameworks' }
unless embed_build_phase
    embed_build_phase = main_target.new_copy_files_build_phase('Embed Frameworks')
    embed_build_phase.symbol_dst_subfolder_spec = :frameworks
    project.save()
end

# # Get frameworks build phase (do this once outside the config loop) for linking
framework_build_phase = main_target.frameworks_build_phase
#Get or create frameworks_group
frameworks_group = project.groups.find { |group| group.display_name == 'Frameworks' }
unless frameworks_group
    frameworks_group = project.new_group('Frameworks')
end
#create a PBXBuildFile
sub_build_file = project.new(Xcodeproj::Project::PBXBuildFile)
sub_build_file.file_ref = sub_target_ref
# add to frameworks group critical
frameworks_group.files << sub_build_file
unless framework_build_phase.files.find { |f| f.file_ref.uuid == sub_target_ref.uuid }
    framework_build_phase.files << sub_build_file
    #framework_build_phase.add_file_reference(ss_target_ref,avoid_duplicates=true)
    puts "Added #{sub_target_ref} framework reference for linking"
    else
    puts "already added"
end

if sub_target_ref
    #Check if already embedded
    unless embed_build_phase.files.find { |f| f.file_ref.path == sub_target_ref.path }
        embed_file = embed_build_phase.add_file_reference(sub_target_ref)
        embed_file.settings = { 'ATTRIBUTES' => ['CodeSignOnCopy', 'RemoveHeadersOnCopy'] }
        puts "Added #{sub_target_ref} framework for embedding"
        else
        puts "already embedded"
    end
end

end

begin
# Open main project
puts "MAIN_PROJECT_PATH: #{MAIN_PROJECT_PATH}"
project = Xcodeproj::Project.open(MAIN_PROJECT_PATH)

# Add subprojects and save after each addition
ss_ref = add_subproject(project, SHADOWSOCKS_PROJECT_PATH)
tun_ref = add_subproject(project, TUN2SOCKS_PROJECT_PATH)
project = Xcodeproj::Project.open(MAIN_PROJECT_PATH)

# Get SoVPNDeps target
sovpn_deps_target = project.targets.find { |t| t.name == 'SoVPNDeps' }
raise "Could not find SoVPNDeps target" unless sovpn_deps_target

# Add shadowsocks project
ss_proj, ss_target = get_subproject(project, SHADOWSOCKS_PROJECT_PATH, 'shadowsocks-libev-shared')
puts "Added shadowsocks project and target"

# Add tun2socks project
tun_proj, tun_target = get_subproject(project, TUN2SOCKS_PROJECT_PATH, 'interface')
puts "Added tun2socks project and target"


# Add dependencies to SoVPNDeps
#check if the target is already in the dependencies
if !sovpn_deps_target.dependencies.include?(ss_target)
    sovpn_deps_target.add_dependency(ss_target)
end
if !sovpn_deps_target.dependencies.include?(tun_target)
    sovpn_deps_target.add_dependency(tun_target)
end

puts "ss_target.product_reference.uuid: #{ss_target.product_reference.uuid}"
puts "tun_target.product_reference.uuid: #{tun_target.product_reference.uuid}"

# project.root_object.project_references hold all subproject references
# in project_references there is a value: Product Groups  contains proxy targets each is referred to subproject target
# in project_references there is a value: Product Groups  contains proxy product ref each is referred to subproject target's product ref
# we can use proxy product ref to link or embeded:
# first add it to frame group ,this is critical,although seems have no effect
# second add it to  embed_build_phase or  framework_build_phase

#find project reference in project
ss_proj_ref = project.root_object.project_references.find { |ref| Pathname(ref[:project_ref].path).basename == Pathname(ss_proj.path).basename }
tun_proj_ref = project.root_object.project_references.find { |ref| Pathname(ref[:project_ref].path).basename == Pathname(tun_proj.path).basename }

puts "ss_proj_ref[:project_ref].uuid: #{ss_proj_ref[:project_ref].uuid}"
puts "tun_proj_ref[:project_ref].uuid: #{tun_proj_ref[:project_ref].uuid}"
ss_target_lists = ss_proj_ref[:product_group].children
tun_target_lists = tun_proj_ref[:product_group].children
#find target in ss_target_lists
ss_target_ref = ss_target_lists.find { |target| target.path == 'shadowsocks-libev.framework' }
tun_target_ref = tun_target_lists.find { |target| target.path == 'interface.framework' }

puts "ss_target_ref: #{ss_target_ref}"
puts "tun_target_ref: #{tun_target_ref}"
puts "ss_target uuid: #{ss_target_ref.uuid}"

add_link_and_embeded(project,sovpn_deps_target,ss_target_ref)

add_link_and_embeded(project,sovpn_deps_target,tun_target_ref)


# Update build settings for each configuration
sovpn_deps_target.build_configurations.each do |config|
    # Update build settings
    config.build_settings['OTHER_LDFLAGS'] ||= ['$(inherited)']
    config.build_settings['OTHER_LDFLAGS'] << '-ObjC' unless config.build_settings['OTHER_LDFLAGS'].include?('-ObjC')
    
    config.build_settings['FRAMEWORK_SEARCH_PATHS'] ||= ['$(inherited)']
    ss_framework_path = '$(SRCROOT)/deps/new/shadowsocks-libev/xcode/lib/$(CONFIGURATION)'
    tun_framework_path = '$(SRCROOT)/deps/new/tun2socks-iOS/xcode/lib/$(CONFIGURATION)'
    
    unless config.build_settings['FRAMEWORK_SEARCH_PATHS'].include?(ss_framework_path)
        config.build_settings['FRAMEWORK_SEARCH_PATHS'] << ss_framework_path
    end
    unless config.build_settings['FRAMEWORK_SEARCH_PATHS'].include?(tun_framework_path)
        config.build_settings['FRAMEWORK_SEARCH_PATHS'] << tun_framework_path
    end
end

# Add build configurations
project.build_configurations.each do |config|
    config.build_settings['HEADER_SEARCH_PATHS'] ||= ['$(inherited)']
    #should search header in the framework path
    #$(EFFECTIVE_PLATFORM_NAME)
    config.build_settings['HEADER_SEARCH_PATHS'] << '$(SRCROOT)/deps/new/shadowsocks-libev/xcode/lib/$(CONFIGURATION)/shadowsocks-libev.framework/Headers'
    config.build_settings['HEADER_SEARCH_PATHS'] << '$(SRCROOT)/deps/new/tun2socks-iOS/xcode/lib/$(CONFIGURATION)/interface.framework/Headers'
    #config.build_settings['HEADER_SEARCH_PATHS'] << '$(SRCROOT)/deps/new/shadowsocks-libev/src'
    #config.build_settings['HEADER_SEARCH_PATHS'] << '$(SRCROOT)/deps/new/tun2socks-iOS/badvpn'
    
    config.build_settings['FRAMEWORK_SEARCH_PATHS'] ||= ['$(inherited)']
    config.build_settings['FRAMEWORK_SEARCH_PATHS'] << '$(SRCROOT)/deps/new/shadowsocks-libev/xcode/lib/$(CONFIGURATION)'
    config.build_settings['FRAMEWORK_SEARCH_PATHS'] << '$(SRCROOT)/deps/new/tun2socks-iOS/xcode/lib/$(CONFIGURATION)'
end

# Save project
project.save
puts "Successfully modified project!"

rescue StandardError => e
puts "Error: #{e.message}"
puts e.backtrace
exit 1

end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate project with sub-projects

7 participants