Skip to content

Commit

Permalink
Compile OpenSSL and libiconv statically into FreeTDS
Browse files Browse the repository at this point in the history
  • Loading branch information
andyundso committed Jan 8, 2025
1 parent f5cd106 commit e768151
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 128 deletions.
20 changes: 10 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ jobs:
- "x64-mingw-ucrt"
name: cross-compile-windows
runs-on: ubuntu-22.04
container:
image: "ghcr.io/rake-compiler/rake-compiler-dock-image:1.7.0-mri-${{ matrix.platform }}"
steps:
- uses: actions/checkout@v4

- run: git config --global --add safe.directory /__w/tiny_tds/tiny_tds # shrug

- name: Install gems
shell: bash
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "2.7"

- name: "Install dependencies"
run: bundle install

- name: Write used versions into file
Expand All @@ -34,14 +34,14 @@ jobs:
uses: actions/cache@v4
with:
path: ports
key: cross-compiled-v3-${{ matrix.platform }}-${{ hashFiles('**/.ports_versions') }}
key: cross-compiled-v7-${{ matrix.platform }}-${{ hashFiles('**/.ports_versions') }}
restore-keys: |
cross-compiled-v3-${{ matrix.platform }}-${{ hashFiles('**/.ports_versions') }}
cross-compiled-v3-${{ matrix.platform }}-
cross-compiled-v7-${{ matrix.platform }}-${{ hashFiles('**/.ports_versions') }}
cross-compiled-v7-${{ matrix.platform }}-
- name: Build gem
shell: bash
run: bundle exec rake gem:for_platform[${{ matrix.platform }}]
run: bundle exec rake gem:native:${{ matrix.platform }}

- uses: actions/upload-artifact@v4
with:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.2.0

* Reduce number of files shipped with precompiled Windows gem

## 3.1.0

* Add Ruby 3.4 to the cross compile list
Expand All @@ -13,6 +17,7 @@
* Add `bigdecimal` to dependencies

## 2.1.7

* Add Ruby 3.3 to the cross compile list

## 2.1.6
Expand Down
46 changes: 21 additions & 25 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,16 @@ require 'rbconfig'
require 'rake'
require 'rake/clean'
require 'rake/extensiontask'
require_relative './ext/tiny_tds/extconsts'

SPEC = Gem::Specification.load(File.expand_path('../tiny_tds.gemspec', __FILE__))

ruby_cc_ucrt_versions = "3.4.0:3.3.5:3.2.0:3.1.0".freeze
ruby_cc_mingw32_versions = "3.0.0:2.7.0".freeze

GEM_PLATFORM_HOSTS = {
'x64-mingw32' => {
host: 'x86_64-w64-mingw32',
ruby_versions: ruby_cc_mingw32_versions
},
'x64-mingw-ucrt' => {
host: 'x86_64-w64-mingw32',
ruby_versions: ruby_cc_ucrt_versions
},
}
CrossLibrary = Struct.new :platform, :openssl_config
CrossLibraries = [
['x64-mingw-ucrt', 'mingw64'],
['x64-mingw32', 'mingw64'],
].map do |platform, openssl_config|
CrossLibrary.new platform, openssl_config
end

# Add our project specific files to clean for a rebuild
CLEAN.include FileList["{ext,lib}/**/*.{so,#{RbConfig::CONFIG['DLEXT']},o}"],
Expand All @@ -35,23 +28,26 @@ Dir['tasks/*.rake'].sort.each { |f| load f }
Rake::ExtensionTask.new('tiny_tds', SPEC) do |ext|
ext.lib_dir = 'lib/tiny_tds'
ext.cross_compile = true
ext.cross_platform = GEM_PLATFORM_HOSTS.keys
ext.cross_platform = CrossLibraries.map(&:platform)

# Add dependent DLLs to the cross gems
ext.cross_compiling do |spec|
# The fat binary gem doesn't depend on the freetds package, since it bundles the library.
spec.metadata.delete('msys2_mingw_dependencies')

# We don't need the sources in a fat binary gem
spec.files = spec.files.reject { |f| f =~ %r{^ports\/archives/} }

# Make sure to include the ports binaries and libraries
spec.files += FileList["ports/#{spec.platform.to_s}/**/**/{bin,lib}/*"].exclude do |f|
File.directory? f
end

spec.files += Dir.glob('exe/*')

spec.files += [
"ports/#{spec.platform.to_s}/bin/libsybdb-5.dll"
]
end

ext.cross_config_options += CrossLibraries.map do |xlib|
{
xlib.platform => [
"--with-cross-build=#{xlib.platform}",
"--with-openssl-platform=#{xlib.openssl_config}"
]
}
end
end

task build: [:clean, :compile]
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.1.0
3.2.0
237 changes: 166 additions & 71 deletions ext/tiny_tds/extconf.rb
Original file line number Diff line number Diff line change
@@ -1,91 +1,186 @@
ENV['RC_ARCHS'] = '' if RUBY_PLATFORM =~ /darwin/

# :stopdoc:

require 'mkmf'
require 'rbconfig'
require_relative './extconsts'

# Shamelessly copied from nokogiri
#

def do_help
print <<HELP
usage: ruby #{$0} [options]
--with-freetds-dir=DIR
Use the freetds library placed under DIR.
HELP
exit! 0
require_relative 'extconsts'

if ENV['MAINTAINER_MODE']
$stderr.puts "Maintainer mode enabled."
$CFLAGS <<
' -Wall' <<
' -ggdb' <<
' -DDEBUG' <<
' -pedantic'
$LDFLAGS <<
' -ggdb'
end

do_help if arg_config('--help')
if gem_platform=with_config("cross-build")
require 'mini_portile2'

openssl_platform = with_config("openssl-platform")

class BuildRecipe < MiniPortile
attr_accessor :gem_platform

def initialize(name, version, files)
super(name, version)
self.files = files
rootdir = File.expand_path('../../..', __FILE__)
self.target = File.join(rootdir, "ports")
self.patch_files = Dir[File.join("patches", self.name, self.version, "*.patch")].sort
end

# this will yield all ports into the same directory, making our path configuration for the linker easier
def port_path
"#{@target}/#{gem_platform}"
end

def cook_and_activate
checkpoint = File.join(self.target, "#{self.name}-#{self.version}-#{gem_platform}.installed")

unless File.exist?(checkpoint)
self.cook
FileUtils.touch checkpoint
end

self.activate
self
end
end

# Make sure to check the ports path for the configured host
architecture = RbConfig::CONFIG['arch']
openssl_recipe = BuildRecipe.new("openssl", OPENSSL_VERSION, [OPENSSL_SOURCE_URI]).tap do |recipe|
class << recipe
attr_accessor :openssl_platform

def configure
envs = []
envs << "CFLAGS=-DDSO_WIN32 -DOPENSSL_THREADS" if RUBY_PLATFORM =~ /mingw|mswin/
envs << "CFLAGS=-fPIC -DOPENSSL_THREADS" if RUBY_PLATFORM =~ /linux/
execute('configure', ['env', *envs, "./Configure", openssl_platform, "threads", "-static", "CROSS_COMPILE=#{host}-", configure_prefix, "--libdir=lib"], altlog: "config.log")
end

def compile
execute('compile', "#{make_cmd} build_libs")
end

def install
execute('install', "#{make_cmd} install_dev")
end
end

recipe.gem_platform = gem_platform
recipe.openssl_platform = openssl_platform
recipe.cook_and_activate
end

project_dir = File.expand_path("../../..", __FILE__)
freetds_ports_dir = File.join(project_dir, 'ports', architecture, 'freetds', FREETDS_VERSION)
freetds_ports_dir = File.expand_path(freetds_ports_dir)
libiconv_recipe = BuildRecipe.new("libiconv", ICONV_VERSION, [ICONV_SOURCE_URI]).tap do |recipe|
recipe.configure_options << "CFLAGS=-fPIC" if RUBY_PLATFORM =~ /linux/
recipe.gem_platform = gem_platform

# Add all the special path searching from the original tiny_tds build
# order is important here! First in, first searched.
DIRS = %w(
/opt/local
/usr/local
)
recipe.cook_and_activate
end

if RbConfig::CONFIG['host_os'] =~ /darwin/i
# Ruby below 2.7 seems to label the host CPU on Apple Silicon as aarch64
# 2.7 and above print is as ARM64
target_host_cpu = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7') ? 'aarch64' : 'arm64'
# remove the ".la" files, otherwise libtool starts to complain when linking into FreeTDS
Dir.glob(File.join(libiconv_recipe.path, "lib", "**", "*.la")).each do |la_file|
File.delete(la_file)
end

if RbConfig::CONFIG['host_cpu'] == target_host_cpu
# Homebrew on Apple Silicon installs into /opt/hombrew
# https://docs.brew.sh/Installation
# On Intel Macs, it is /usr/local, so no changes necessary to DIRS
DIRS.unshift("/opt/homebrew")
freetds_recipe = BuildRecipe.new("freetds", FREETDS_VERSION, [FREETDS_SOURCE_URI]).tap do |recipe|
class << recipe
def configure_defaults
[
"--host=#{@host}",
"--enable-shared",
"--disable-static",
"--disable-odbc",
"--enable-sspi",
]
end
end

# i am not 100% what is going on behind the scenes
# it seems that FreeTDS build system prefers OPENSSL_CFLAGS and OPENSSL_LIBS
# but the linker still relies on LIBS and CPPFLAGS
# removing one or the other leads to build failures in any case of FreeTDS
recipe.configure_options << "CFLAGS=-fPIC" if RUBY_PLATFORM =~ /linux/
recipe.configure_options << "LDFLAGS=-L#{openssl_recipe.path}/lib"
recipe.configure_options << "LIBS=-liconv -lssl -lcrypto -lwsock32 -lgdi32 -lws2_32 -lcrypt32"
recipe.configure_options << "CPPFLAGS=-I#{openssl_recipe.path}/include"

recipe.configure_options << "OPENSSL_CFLAGS=-L#{openssl_recipe.path}/lib"
recipe.configure_options << "OPENSSL_LIBS=-lssl -lcrypto -lwsock32 -lgdi32 -lws2_32 -lcrypt32"

recipe.configure_options << "--with-openssl=#{openssl_recipe.path}"
recipe.configure_options << "--with-libiconv-prefix=#{libiconv_recipe.path}"

recipe.gem_platform = gem_platform
recipe.cook_and_activate
end
end

if ENV["RI_DEVKIT"] && ENV["MINGW_PREFIX"] # RubyInstaller Support
DIRS.unshift(File.join(ENV["RI_DEVKIT"], ENV["MINGW_PREFIX"]))
end
ENV["LDFLAGS"] = "-Wl,-rpath -Wl,#{freetds_recipe.path}/lib"
dir_config('freetds', "#{freetds_recipe.path}/include", "#{freetds_recipe.path}/lib")
else
# Make sure to check the ports path for the configured host
architecture = RbConfig::CONFIG['arch']

project_dir = File.expand_path("../../..", __FILE__)
freetds_ports_dir = File.join(project_dir, 'ports', architecture, 'freetds', FREETDS_VERSION)
freetds_ports_dir = File.expand_path(freetds_ports_dir)

# Add all the special path searching from the original tiny_tds build
# order is important here! First in, first searched.
DIRS = %w(
/opt/local
/usr/local
)

if RbConfig::CONFIG['host_os'] =~ /darwin/i
# Ruby below 2.7 seems to label the host CPU on Apple Silicon as aarch64
# 2.7 and above print is as ARM64
target_host_cpu = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7') ? 'aarch64' : 'arm64'

if RbConfig::CONFIG['host_cpu'] == target_host_cpu
# Homebrew on Apple Silicon installs into /opt/hombrew
# https://docs.brew.sh/Installation
# On Intel Macs, it is /usr/local, so no changes necessary to DIRS
DIRS.unshift("/opt/homebrew")
end
end

# Add the ports directory if it exists for local developer builds
DIRS.unshift(freetds_ports_dir) if File.directory?(freetds_ports_dir)
if ENV["RI_DEVKIT"] && ENV["MINGW_PREFIX"] # RubyInstaller Support
DIRS.unshift(File.join(ENV["RI_DEVKIT"], ENV["MINGW_PREFIX"]))
end

# Grab freetds environment variable for use by people on services like
# Heroku who they can't easily use bundler config to set directories
DIRS.unshift(ENV['FREETDS_DIR']) if ENV.has_key?('FREETDS_DIR')
# Add the ports directory if it exists for local developer builds
DIRS.unshift(freetds_ports_dir) if File.directory?(freetds_ports_dir)

# Add the search paths for freetds configured above
ldirs = DIRS.flat_map do |path|
ldir = "#{path}/lib"
[ldir, "#{ldir}/freetds"]
end
# Grab freetds environment variable for use by people on services like
# Heroku who they can't easily use bundler config to set directories
DIRS.unshift(ENV['FREETDS_DIR']) if ENV.has_key?('FREETDS_DIR')

idirs = DIRS.flat_map do |path|
idir = "#{path}/include"
[idir, "#{idir}/freetds"]
end
# Add the search paths for freetds configured above
ldirs = DIRS.flat_map do |path|
ldir = "#{path}/lib"
[ldir, "#{ldir}/freetds"]
end

puts "looking for freetds headers in the following directories:\n#{idirs.map{|a| " - #{a}\n"}.join}"
puts "looking for freetds library in the following directories:\n#{ldirs.map{|a| " - #{a}\n"}.join}"
dir_config('freetds', idirs, ldirs)

have_dependencies = [
find_header('sybfront.h'),
find_header('sybdb.h'),
find_library('sybdb', 'tdsdbopen'),
find_library('sybdb', 'dbanydatecrack')
].inject(true) do |memo, current|
memo && current
idirs = DIRS.flat_map do |path|
idir = "#{path}/include"
[idir, "#{idir}/freetds"]
end

puts "looking for freetds headers in the following directories:\n#{idirs.map{|a| " - #{a}\n"}.join}"
puts "looking for freetds library in the following directories:\n#{ldirs.map{|a| " - #{a}\n"}.join}"
dir_config('freetds', idirs, ldirs)
end

unless have_dependencies
abort 'Failed! Do you have FreeTDS 1.0.0 or higher installed?'
if /solaris/ =~ RUBY_PLATFORM
append_cppflags( '-D__EXTENSIONS__' )
end

create_makefile('tiny_tds/tiny_tds')
find_header('sybfront.h') or abort "Can't find the 'sybfront.h' header"
find_header('sybdb.h') or abort "Can't find the 'sybdb.h' header"

unless find_library('sybdb', 'dbanydatecrack')
abort "Failed! Do you have FreeTDS 1.0.0 or higher installed?"
end

# :startdoc:
create_makefile("tiny_tds/tiny_tds")
2 changes: 1 addition & 1 deletion lib/tiny_tds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
end
end

# Temporary add bin directories for DLL search, so that freetds DLLs can be found.
# Temporary add bin directories for DLL search, so that freetds DLL can be found.
add_dll_paths.call( TinyTds::Gem.ports_bin_paths ) do
begin
require "tiny_tds/#{ver}/tiny_tds"
Expand Down
Loading

0 comments on commit e768151

Please sign in to comment.