mtwilliams/ryb

View on GitHub
lib/ryb/ninja.rb

Summary

Maintainability
C
7 hrs
Test Coverage
module Ryb
  module Ninja
    class Generator
      def initialize(rybfile, opts)
        @rybfile = rybfile
        @root = opts.fetch(:root, '.')
        @build = opts.fetch(:build, '_build')
        @ninjafile = ::Ninja::File.new
        @path = opts.fetch(:ninjafile, "#{@root}/build.ninja")

        # HACK(mtwilliams): Create directory structure.
        require 'fileutils'
        FileUtils.mkdir_p(["#@build/bin", "#@build/lib", "#@build/obj"])

        # HACK(mtwilliams): Assume VisualStudio.
        @vs = ::VisualStudio.latest
        @vc = @vs.products[:c_and_cpp]
      end

      def generate
        @ninjafile.rule "mv", "move /Y $in $out"
        @ninjafile.rule "cp", "move /Y /B $in $out"

        @ninjafile.save(@path)
      end

      # TODO(mtwilliams): Generate phony commands that proxy to all triplets.
      def on_project(project)
        puts "=> #{project.name.pretty || project.name}"
      end

      def on_product(project, product)
        puts " -> Generating for #{product.name.pretty || product.name}"
      end

      def on_project_triplet(project, tripletised)
      end

      def on_product_triplet(project, product, tripletised)
        tripletised = tripletised.()

        config = tripletised.configuration.name.to_sym
        platform = tripletised.platform.name.to_sym
        arch = tripletised.architecture.name.to_sym

        puts "  ~> #{config} for #{platform} (#{arch})"

        name = product.name.canonicalize
        namespace = "#{name}_#{tripletised.triplet.join('_')}"

        sdk = @vc.sdks[:windows].select{|sdk| sdk.version.to_f >= 7.1}.first
        sys_include_paths = @vc.includes + sdk.includes
        sys_lib_paths = @vc.libraries[arch] + sdk.libraries[arch]

        @ninjafile.variable "#{namespace}_cflags_sys", VisualStudio::Compiler.include_paths_to_flags(sys_include_paths).join(' ')
        @ninjafile.variable "#{namespace}_cxxflags_sys", "$#{namespace}_cflags_sys"
        @ninjafile.variable "#{namespace}_ldflags_sys", VisualStudio::Linker.library_paths_to_flags(sys_lib_paths).join(' ')

        cflags = cflags_for_product(project, product, tripletised)
        arflags = arflags_for_product(project, product, tripletised)
        ldflags = ldflags_for_product(project, product, tripletised)

        @ninjafile.variable "#{namespace}_cflags", cflags.join(' ')
        @ninjafile.variable "#{namespace}_cxxflags", "$#{namespace}_cflags"
        @ninjafile.variable "#{namespace}_arflags", arflags.join(' ')
        @ninjafile.variable "#{namespace}_ldflags", ldflags.join(' ')

        sources = sources_for_product(product, tripletised)
        @ninjafile.variable "#{namespace}_sources", sources.map{|src| "#@root/#{src.path}"}.join(' ')

        deps = deps_for_product(product, tripletised)
        @ninjafile.variable "#{namespace}_deps", deps.join(' ')

        artifacts = artifacts_for_product(product, tripletised)
        @ninjafile.variable "#{namespace}_artifacts", artifacts.join(' ')
        @ninjafile.variable("#{namespace}_as_linkable", artifacts[0]) if product.is_a? Ryb::Library
        @ninjafile.variable "#{namespace}_symbols", artifacts[0].gsub(/\.(exe|lib|dll)/, ".pdb")

        # https://github.com/ninja-build/ninja/blob/d1763746b65cc7349d4ed9478befdb651aa24589/src/msvc_helper_main-win32.cc#L38
        env_block = "#@build/#{namespace}.env_block"
        env = @vc.environment(target: {platform: :windows, architecture: arch}, base: environment)
        # "PATH=#{(@vc.binaries[arch] + sdk.binaries[arch]).join(';')}\0"
        File.write(env_block, env.map{|env_var, value| "#{env_var}=#{value}"}.join("\0"))

        @ninjafile.rule(
          "cc_#{namespace}",
          "ninja -t msvc -e #{env_block} -- cl.exe /showIncludes $#{namespace}_cflags_sys $#{namespace}_cflags /Fd$#{namespace}_symbols /Fo$out /Tc$in",
          dependencies: :msvc
        )

        @ninjafile.rule(
          "cxx_#{namespace}",
          "ninja -t msvc -e #{env_block} -- cl.exe /showIncludes $#{namespace}_cxxflags_sys $#{namespace}_cxxflags /Fd$#{namespace}_symbols /Fo$out /Tp$in",
          dependencies: :msvc
        )

        @ninjafile.rule(
          "ar_#{namespace}",
          "ninja -t msvc -e #{env_block} -- lib.exe @$out.rsp",
          response_file: "$#{namespace}_arflags /OUT:$out $in"
        )

        @ninjafile.rule(
          "ld_#{namespace}",
          "ninja -t msvc -e #{env_block} -- link.exe @$out.rsp",
          response_file: "$#{namespace}_ldflags_sys $#{namespace}_ldflags /OUT:$out $#{namespace}_deps $in"
        )

        @ninjafile.rule(
          "so_#{namespace}",
          "ninja -t msvc -e #{env_block} -- link.exe @$out.rsp",
          response_file: "$#{namespace}_ldflags_sys $#{namespace}_ldflags /DLL /IMPLIB:$#{namespace}_as_linkable /OUT:$out $#{namespace}_deps $in"
        )

        c_sources = sources.select{|src| src.language == :c}.map(&:path).map{|src| "#@root/#{src}"}
        c_objects = c_sources.map{|src| src.gsub(/\.(c)/, ".#{namespace}.obj")}.map{|obj| "#@build/obj/#{obj}"}
        cxx_sources = sources.select{|src| src.language == :cpp}.map(&:path).map{|src| "#@root/#{src}"}
        cxx_objects = cxx_sources.map{|src| src.gsub(/\.(cc|cpp|cxx|c++)/, ".#{namespace}.obj")}.map{|obj| "#@build/obj/#{obj}"}

        @ninjafile.build "cc_#{namespace}", Hash[c_objects.zip(c_sources)]
        @ninjafile.build "cxx_#{namespace}", Hash[cxx_objects.zip(cxx_sources)]

        case product
          when Ryb::Application
            @ninjafile.build "ld_#{namespace}", {artifacts[0] => c_objects + cxx_objects}
            @ninjafile.alias namespace, artifacts[0]
          when Ryb::Library
            case product.linkage
              when :static
                @ninjafile.build "ar_#{namespace}", {artifacts[0] => c_objects + cxx_objects}
                @ninjafile.alias namespace, artifacts[0]
              when :dynamic
                @ninjafile.build "so_#{namespace}", {artifacts[1] => c_objects + cxx_objects}
                @ninjafile.alias namespace, artifacts[1]
              end
          end
      end

      private
        def environment
          variables = [
            'ALLUSERSPROFILE',
            'APPDATA',
            'CommonProgramFiles',
            'CommonProgramFiles(x86)',
            'CommonProgramW6432',
            'ComSpec',
            'HOMEDRIVE',
            'HOMEPATH',
            'LOCALAPPDATA',
            'Path',
            'PATHEXT',
            'ProgramData',
            'ProgramFiles',
            'ProgramFiles(x86)',
            'ProgramW6432',
            'PUBLIC',
            'SystemDrive',
            'SystemRoot',
            'TEMP',
            'TMP',
            'USERDOMAIN',
            'USERNAME',
            'USERPROFILE',
            'windir'
          ]

          @environment ||= begin
            require 'win32/registry'
            ::Win32::Registry::HKEY_LOCAL_MACHINE.open("SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", ::Win32::Registry::KEY_READ) do |originals|
              variables.map { |variable|
                value = begin
                  originals[variable]
                rescue
                  ENV[variable]
                end
                [variable, value]
              }.to_h
            end
          end
        end

        def sources_for_product(product, tripletised)
          sources = [*product.sources] |
                    [*tripletised.configuration.sources] |
                    [*tripletised.platform.sources] |
                    [*tripletised.architecture.sources]
          sources.reject{|src| src.inconsequential}.compact.uniq
        end

        def deps_for_product(product, tripletised)
          deps = [*product.dependencies] |
                 [*tripletised.configuration.dependencies] |
                 [*tripletised.platform.dependencies] |
                 [*tripletised.architecture.dependencies]
          deps.map do |dep|
            case dep
              when Ryb::InternalDependency
                "${#{dep.product}_#{tripletised.triplet.join('_')}_as_linkable}"
              when Ryb::ExternalDependency
                triplet = [tripletised.configuration, tripletised.platform, tripletised.architecture].map(&:name).map(&:to_sym)
                mangled = dep.mangled(*triplet)
                "#{mangled}.lib"
              end
          end
        end

        def artifacts_for_product(product, tripletised)
          name = product.name.canonicalize
          config_suffix = tripletised.configuration.suffix || "_#{tripletised.configuration.name}"
          platform_suffix = tripletised.platform.suffix || "_#{tripletised.platform.name}"
          arch_suffix = tripletised.architecture.suffix || "_#{tripletised.architecture.name}"
          suffix = [config_suffix, platform_suffix, arch_suffix].join('')

          if product.is_a? Ryb::Application
            ["#@build/bin/#{name}#{suffix}.exe"]
          elsif product.is_a? Ryb::Library
            case product.linkage
              when :static
                ["#@build/lib/#{name}#{suffix}.lib"]
              when :dynamic
                ["#@build/lib/#{name}#{suffix}.lib", "#@build/bin/#{name}#{suffix}.dll"]
              end
          end
        end

      private
        def cflags_for_product(project, product, tripletised)
          flags = VisualStudio::Compiler::STANDARD_FLAGS +
                  cflags_for_configuration(tripletised.configuration) +
                  cflags_for_platform(tripletised.platform) +
                  cflags_for_architecture(tripletised.architecture)
          flags += VisualStudio::Compiler.include_paths_to_flags(project.paths.includes) if project.paths
          flags += VisualStudio::Compiler.include_paths_to_flags(product.paths.includes) if product.paths
          flags += VisualStudio::Compiler.defines_to_flags(project.defines)
          flags += VisualStudio::Compiler.defines_to_flags(product.defines)
          flags += VisualStudio::Compiler.architecture_to_flags(tripletised.architecture.name.to_sym)
          flags
        end

        def arflags_for_product(_project, _product, _tripletised)
          %w{/nologo}
        end

        def ldflags_for_product(project, product, tripletised)
          flags = VisualStudio::Linker::STANDARD_FLAGS +
                  ldflags_for_configuration(tripletised.configuration) +
                  ldflags_for_platform(tripletised.platform) +
                  ldflags_for_architecture(tripletised.architecture)
          flags += VisualStudio::Linker.library_paths_to_flags(project.paths.libraries) if project.paths
          flags += VisualStudio::Linker.library_paths_to_flags(product.paths.libraries) if product.paths
          flags += VisualStudio::Linker.architecture_to_flags(tripletised.architecture.name.to_sym)
          # TODO: Linkage.
          flags
        end

      private
        def cflags_for_configuration(config)
          flags = []
          # TODO(mtwilliams): @toolchains.for(target)
          flags += VisualStudio::Compiler.include_paths_to_flags(config.paths.includes) if config.paths
          flags += VisualStudio::Compiler.defines_to_flags(config.defines)
          flags += VisualStudio::Compiler.treat_warnings_as_errors_to_flag(config.treat_warnings_as_errors)
          flags += VisualStudio::Compiler.generate_debug_symbols_to_flag(config.generate_debug_symbols)
          flags += VisualStudio::Compiler.optimization_to_flags(config.optimize)
          flags
        end

        def cflags_for_platform(platform)
          flags = []
          # TODO(mtwilliams): @toolchains.for(target)
          flags += VisualStudio::Compiler.include_paths_to_flags(platform.paths.includes) if platform.paths
          flags += VisualStudio::Compiler.defines_to_flags(platform.defines)
          flags
        end

        def cflags_for_architecture(arch)
          flags = []
          # TODO(mtwilliams): @toolchains.for(target)
          flags += VisualStudio::Compiler.architecture_to_flags(arch.name.canonicalize.to_sym)
          flags += VisualStudio::Compiler.include_paths_to_flags(arch.paths.includes) if arch.paths
          flags += VisualStudio::Compiler.defines_to_flags(arch.defines)
          flags
        end

        def ldflags_for_configuration(config)
          flags = []
          # TODO(mtwilliams): @toolchains.for(target)
          flags += VisualStudio::Linker.library_paths_to_flags(config.paths.libraries) if config.paths
          flags += VisualStudio::Linker.generate_debug_symbols_to_flag(config.generate_debug_symbols)
          flags
        end

        def ldflags_for_platform(platform)
          flags = []
          # TODO(mtwilliams): @toolchains.for(target)
          flags += VisualStudio::Linker.library_paths_to_flags(platform.paths.libraries) if platform.paths
          flags
        end

        def ldflags_for_architecture(arch)
          flags = []
          # TODO(mtwilliams): @toolchains.for(target)
          flags += VisualStudio::Linker.library_paths_to_flags(arch.paths.libraries) if arch.paths
          flags
        end
    end

    def self.generate_from(rybfile, opts={})
      puts "Generating..."
      generator = ::Ryb::Ninja::Generator.new(rybfile, opts)
      ::Rybfile::Walker.new(generator, rybfile, opts).walk
      generator.generate
      puts "Done!"
    end
  end
end