gradle / gradle-native

The home of Gradle's support for natively compiled languages
https://blog.gradle.org/introducing-the-new-cpp-plugins
Apache License 2.0
93 stars 8 forks source link

Feature request: Better support for different versions of Microsoft VC++ compilers as different toolchains or targets #1039

Open philmcardle opened 7 years ago

philmcardle commented 7 years ago

I don't know what the appropriate way to solve this is, so a bit of a scattershot for the title, sorry.

Expected Behavior

Gradle should supporting targeting different versions of the Microsoft VC++ compiler with the same flexibility as it supports separately targeting VisualCpp, Gcc, Clang and others right now.

Current Behavior

Gradle treats all versions of the VC++ compiler as equivalent, when they're not in fact cross-compatible, requiring native developers to go to great lengths to separately target each version of VS for the build steps, and when sourcing the dependencies.

Context

I'm not the first person to report this on the forums, but I posted this topic most recently, for context: https://discuss.gradle.org/t/native-trying-to-constrain-which-platforms-a-toolchain-supports/20424

At the moment, I have a project which has to be built for Windows x64 using VS2013, Windows x64 using VS2015 and Linux x64 using gcc, with prebuilt dependencies having been built for each of these already. It will likely expand to more later.

I'll work around it by coercing Gradle to only configure one C++ toolchain at a time, using pre-defined / command-line project properties. Not ideal, and a betrayal of one of Gradle's most useful features, but I can't see another way right now.

lacasseio commented 7 years ago

Thanks Phil for the feature request. It is a limitation with the Visual Cpp toolchain. Your current workaround is the way to go for now, until we improve the model.

philmcardle commented 7 years ago

In my above comment, I suggested that I would workaround this by coercing Gradle to only configure one C++ toolchain at a time, using pre-defined / command-line project properties, but in researching GH 906 I stumbled across setToolChain on InstallExecutable, CreateStaticLibrary, AbstractNativeCompileTask, and AbstractLinkTask and I'm wondering whether I could instead configure both my vs2013 and vs2015 toolchains:

apply plugin: 'cpp'

model {

    toolChains  {

        vs2015(VisualCpp) {
            installDir = 'C:/Program Files (x86)/Microsoft Visual Studio 14.0'
        }

        vs2013(VisualCpp) {
            installDir = 'C:/Program Files (x86)/Microsoft Visual Studio 12.0'
        }

    }        

    platforms {
        windows_x64_vs2015 {
            architecture 'x86_64'
            operatingSystem 'windows'
            ext.toolChain = 'vs2015'
        }
        windows_x64_vs2013 {
            architecture 'x86_64'
            operatingSystem 'windows'
            ext.toolChain = 'vs2013'
        }
    }

    components {

        TestProject(NativeLibrarySpec) {
            targetPlatform 'windows_x64_vs2015'
            targetPlatform 'windows_x64_vs2013'
        }
    }

}

..and then call this setToolChain method with the different VS toolchains above on each of the tasks configured for each platform?

e.g. compileTestProjectWindows_x64_vs2013SharedLibraryTestProjectCpp, compileTestProjectWindows_x64_vs2015SharedLibraryTestProjectCpp and so on.

I'm going to try this later, but, just talking out loud in case a) it works b) it's something that's likely to be unsupported later and I should avoid.

philmcardle commented 7 years ago

Okay, the above doesn't work.

I have the usual issue with competing configuration being applied at different times - much of the configuration that depends on knowing which toolchain will be used to run a given task is applied before I can change the toolchains on the tasks.

Going by options.txt (and the subsequent output), the compile tasks seem to be completely unaffected, and the link tasks are partially affected, so I end up calling a vs2013 linker on code compiled with vs2015. I can provide example code if you're curious, but, you probably saw it coming πŸ™‚

I can't tell how well any other tasks were affected, in this scenario, but, not workable.

@lacasseio (and apologies for the tag) Is there any way I can get to call setToolChain before the rest of the configuration? (assuming that my idea is otherwise viable)

lacasseio commented 7 years ago

Thanks @philmcardle for the awesome work you are doing on this and no worries for the tag. Since linker tasks are created after the component container is closed, I would suggest mutating the binaries matching your platform for each component like the following instead of changing the AbstractLinkTask task:

import org.gradle.nativeplatform.internal.NativeBinarySpecInternal

apply plugin: "cpp"

model {
    components {
        main(NativeExecutableSpec)
        withType(NativeComponentSpec) { component ->
            component.binaries.afterEach(NativeBinarySpecInternal) { binary ->
                binary.setToolChain(null)
            }
        }
    }
}

Give it a try and I would be very curious to know if this work.

philmcardle commented 7 years ago

Hahaha, that works πŸ˜…

I didn't need the use of NativeBinarySpecInternal, so I'd be curious to know what that would have been for.

I'm able to do this across my entire multi-project build with the following code:

model {

    platforms {
        windows_x64_vs2015 {
            architecture 'x86_64'
            operatingSystem 'windows'
            ext.toolChain = 'vs2015'
        }
        windows_x64_vs2013 {
            architecture 'x86_64'
            operatingSystem 'windows'
            ext.toolChain = 'vs2013'
        }            
    }

    components {

        withType(NativeComponentSpec) {

            targetPlatform 'windows_x64_vs2015'
            targetPlatform 'windows_x64_vs2013'

            binaries {
                afterEach { binary ->
                    if (binary.name =~ /vs2013/) {
                        binary.setToolChain(toolChains.vs2013)
                    }
                }
            }
        }
    }
}

I've included my custom platforms to give the code a little more context. I should also call setToolChain explicitly for vs2015 rather than relying on it defaulting to it by way of the order of defined toolchains, but, as a proof of concept πŸ™‚

I have more than one BuildType defined, so my binary names are of the form:

windows_x64_vs2013DebugSharedLibrary windows_x64_vs2015DebugSharedLibrary

..and so on.

I've confirmed that all the compiler and linker commands are correct for all targets and additionally that my prebuilt library code alluded to in gradle/gradle#823 continues to work fine (which, you would expect - but sometimes the simultaneous configuration makes this hilarious). I've included it here for reference, and for anyone else considering a similar approach:

repositories {

    libs(PrebuiltLibraries) {

        GoogleTest {
            headers.srcDir "$rootProject.projectDir/Dependencies/GoogleTest-1.8.0/googletest/include"
            binaries.withType(StaticLibraryBinary) {

                def googleTestBuildType
                switch (buildType) {
                    case buildTypes.Debug:
                        googleTestBuildType = 'debug'
                        break
                    case buildTypes.Release:
                        googleTestBuildType = 'release'
                        break
                }

                // Bug Workaround - https://github.com/gradle/gradle/issues/823
                // Check for the existence of our custom property before trying to use it in configuration
                if (targetPlatform.hasProperty('toolChain')) {
                    def googleTestCompiler = targetPlatform.toolChain
                    def googleTestLibName

                    switch (targetPlatform.operatingSystem) {
                        case { it.isWindows() }:
                            googleTestLibName = 'gtest.lib'
                            break
                        case { it.isLinux() } :
                            googleTestLibName = 'libgtest.a'
                            break
                    }

                    staticLibraryFile = file("$rootProject.projectDir/Dependencies/GoogleTest-1.8.0/googletest/lib/${googleTestCompiler}/${googleTestBuildType}/${googleTestLibName}")
                }
            }
        }
    }
}
philmcardle commented 7 years ago

Additionally, the toolChain changes are automatically propagated to the respective GoogleTest testSuites, though I have no idea which code is handling that I am grateful nonetheless πŸ™‚

lacasseio commented 7 years ago

Thanks for posting your solution here Phil. It will be very helpful for everyone until we get a change to improve this specific scenario.

I referenced NativeBinarySpecInternal in my example because setToolChain is a method of that class. There are several reasons writing Gradle code that way which is more verbose:

  1. Groovy doesn't care about the interface. Groovy will act on the actual class instance of the binary (DefaultSharedLibraryBinarySpec, DefaultStaticLibraryBinarySpec and DefaultNativeExecutableSpec). Those classes implement both the public and internal API of Gradle. Even if the API specify SharedLibraryBinarySpec, Groovy ignore that and tries to call the method you want on the object. Sometimes that method is available in the internal API and everything will work without been able to find the public documentation. It causes confusion to the less experienced user.
  2. Make it explicit when internal API are used. At some point later, you will want to access what internal API you are using. Having an explicit filter on containers that specify internal classes will help grepping your code for that knowledge.
  3. Explicit filter of every container. Containers can contain any type you may want that inherit from its base type. For example, the binaries contain can contain anything that is a BinarySpec. Future development, either by the Gradle team or by your team, may decide to add a custom type to a container. That custom type may not have setToolChain which may break your code unexpectedly. Filtering helps been explicit in the outcome of your configuration.

I hope this answer your question. Again, thanks a lot for posting your solution.

philmcardle commented 7 years ago

I didn't even realise it was internal, but I can see now that it's not on the javadoc pages (and I can see that I looked for this exact method before).

I'll amend my own code to use that, as you suggest, thanks.