Using use_libiconv on Mac with MacPorts

My question is: How are flags meant to be persistent, at least for host_flags?

What I’m trying to do:

I’ve compiled Crystal 1.17.1 with clang-20 from MacPorts. The MacPorts provided libiconv uses the libiconv function names, rather than iconv. This seems to be already well-handled by the Crystal standard library, but I’m having run-time problems.

My Makefile.local is:

progress = 1
threads = 8
verbose = 1
interpreter = 1
FLAGS = --verbose -Duse_libiconv
LDFLAGS = -rpath /opt/local/lib -rpath /opt/local/libexec/llvm-20/lib
export LDFLAGS
LLVM_CONFIG = /opt/local/bin/llvm-config-mp-20
export LLVM_CONFIG
CC = /opt/local/bin/clang-mp-20
export CC
LD = /opt/local/bin/lld-link-mp-20
export LD

The resulting crystal fails on basic I/O tests that require iconv due to the “use_libiconv” flag not being set when the compiler runs.

Example failure in make test:

  1) ARGV accepts UTF-8 command-line arguments
     Failure/Error: fail "Compiler command `#{compiler} #{args.join(" ")}` failed with status #{status}.#{"\n" if output}#{output}"

       Compiler command `bin/crystal build -o /var/folders/t8/fscs9s514y50wy52_d38qrlc0000gn/T/cr-spec-31c17fe7/kernel/executable_file /var/folders/t8/fscs9s514y50wy52_d38qrlc0000gn/T/cr-spec-31c17fe7/kernel/source_file` failed with status 1.
       Using compiled compiler at .build/crystal
       ld: Undefined symbols:
         _iconv, referenced from:
             _*Crystal::Iconv#convert<Pointer(Pointer(UInt8)), Pointer(UInt64), Pointer(Pointer(UInt8)), Pointer(UInt64)>:UInt64 in C-rystal5858I-conv.o0.o
         _iconv_close, referenced from:
             _*Crystal::Iconv#close:Nil in C-rystal5858I-conv.o0.o
         _iconv_open, referenced from:
             _*Crystal::Iconv#initialize<String, String, (Symbol | Nil)>:Nil in C-rystal5858I-conv.o0.o
       clang: error: linker command failed with exit code 1 (use -v to see invocation)
       Error: execution of command failed with exit status 1: /opt/local/bin/clang-mp-20 "${@}" -o /var/folders/t8/fscs9s514y50wy52_d38qrlc0000gn/T/cr-spec-31c17fe7/kernel/executable_file  -rdynamic -L/usr/local/bin/../lib/crystal -L/opt/local/lib -lgc -lpthread -liconv

This can be demonstrated more simply from a simple command-line to evaluate the use_libconv flag:

% crystal eval 'puts {{ flag?(:use_libiconv) }}'                    
ld: Undefined symbols:
  _iconv, referenced from:
      _*Crystal::Iconv#convert<Pointer(Pointer(UInt8)), Pointer(UInt64), Pointer(Pointer(UInt8)), Pointer(UInt64)>:UInt64 in C-rystal5858I-conv.o0.o
  _iconv_close, referenced from:
      _*Crystal::Iconv#close:Nil in C-rystal5858I-conv.o0.o
  _iconv_open, referenced from:
      _*Crystal::Iconv#initialize<String, String, (Symbol | Nil)>:Nil in C-rystal5858I-conv.o0.o
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Error: execution of command failed with exit status 1: cc "${@}" -o /Users/jkridner/.cache/crystal/crystal-run-eval.tmp  -rdynamic -L/usr/local/bin/../lib/crystal -L/opt/local/lib -lgc -lpthread -liconv
% crystal eval -Duse_libiconv 'puts {{ flag?(:use_libiconv) }}' 
true
% crystal eval -Duse_libiconv 'puts {{ host_flag?(:use_libiconv) }}'
false

So, why doesn’t Crystal remember that I want the compiler to use libiconv? How should I be running the tests?

OK, now that I’ve posed the question, I did go and look at what the MacPorts maintainer did to build crystal in the MacPorts distro. I’m personally not familiar with Portfile syntax or style, but what seems clear is:

  • the dependency is in the libiconv provided within the distro,
  • they similarly fixed-up the rpath for llvm,
  • they did NOT use the use_libiconv flag,
  • they copied src/lib_c/*-openbsd/c/iconv.cr to src/lib_c/*-darwin/c/iconv.cr.

Not surprising, the OpenBSD iconv.cr uses the libiconv function names. This hardly seems like a suitable work-around, so how is this supposed to work? Should MacPorts have a new target that is not “-darwin” or should Crystal somehow be remembering use_libiconv?

Finding these downstream hacks has become a real pet peeve for me, so I hope we can clear this up together.

From the perspective of trying to create some more targets for Crystal, it would be just good for me to understand the mindset behind using flags, if they are always considered to be ephemeral, and what should really go into a lib_c target.

For reference, my installed ports are:

% port installed requested      
The following ports are currently installed:
  binaryen @123_0 (active)
  bison @3.8.2_2+universal (active)
  boehmgc @8.2.8_1 (active)
  cargo @0.89.0_0 (active)
  clang-20 @20.1.8_0+analyzer (active)
  clang_select @2.4_0 (active)
  cmake @3.31.7_0+universal (active)
  coreutils @9.5_1 (active)
  create-dmg @1.2.2_0 (active)
  curl @8.13.0_0+brotli+darwinssl+http2+idn+psl+universal+zstd (active)
  curl-ca-bundle @8.13.0_0 (active)
  dfu-util @0.11_0 (active)
  flex @2.6.4_0+universal (active)
  gawk @5.3.2_0+universal (active)
  glib2 @2.78.4_2+universal+x11 (active)
  go @1.24.5_0 (active)
  gptfdisk @1.0.9_1 (active)
  harfbuzz @11.2.1_1 (active)
  hidapi @0.12.0_0 (active)
  ImageMagick @6.9.13-21_0+x11 (active)
  libarchive @3.8.1_0+universal (active)
  libiconv @1.17_0
  llvm-20 @20.1.8_0 (active)
  llvm-devel @20240308-6a0618a0_0 (active)
  llvm_select @2_1 (active)
  md4c @0.5.2_0+universal (active)
  nasm @2.16.03_0 (active)
  nbd @3.20_0 (active)
  ninja @1.12.1_1+universal (active)
  nmap @7.97_0+pcre+ssl (active)
  opencv4-devel @4.9.0_5 (active)
  openssh @10.0p2_2 (active)
  pandoc @3.7.0.2_0 (active)
  pkgconfig @0.29.2_0 (active)
  py-hid @1.0.5_0 (active)
  py-jupyter @1.0.0_3 (active)
  py-pygraphviz @1.11_0 (active)
  py27-tkinter @2.7.18_0 (active)
  py27-virtualenv @20.15.1_0 (active)
  py313-markupsafe @3.0.2_0 (active)
  py313-pygraphviz @1.11_0 (active)
  py313-setuptools @80.9.0_0 (active)
  python27 @2.7.18_10+lto+optimizations+universal (active)
  qemu @10.0.2_0+cocoa+curses+spice+spice_protocol+target_arm+target_i386+target_x86_64+usb+vnc (active)
  rust @1.88.0_0 (active)
  swig @4.3.1_0 (active)
  swig-python @4.3.1_0 (active)
  tio @3.9_0 (active)
  tree @2.2.1_0 (active)
  wasm3 @0.5.0_0 (active)
  wget @1.25.0_1+gnutls (active)

Here’s a bit more of an update as I started looking into what I might propose as a patch to the library.

It didn’t take long for me to discover the CRYSTAL_OPTS environment variable. I was trying to figure out how I might detect I was running a MacPorts executable and I haven’t figured that out yet, but it did point me in the direction of looking at environment variables.

This gave me a trivial way to at least get make test to pass:

% export CRYSTAL_OPTS="-D use_libiconv"
% make test
...
Finished in 2.26 seconds
717 examples, 0 failures, 0 errors, 0 pending
...

So, .build/primatives_spec is fine. And, .build/std_spec is fine, with 23 pending tests and all others passing.

BUT, .build/compiler_specstill fails.

AND, it doesn’t answer what the right approach should be.

The proposal from ChatGPT/DeepWIki was as follows

  1. MacPorts Portfile
    Remove the current hack that copies iconv.cr from OpenBSD to Darwin.
    Instead, build Crystal with FLAGS="-Duse_libiconv" (also pass it during tests).

  2. Crystal stdlib (iconv.cr)
    Add host_flag?(:use_libiconv) so that a compiler built with this flag always uses libiconv_*.

-{% if flag?(:use_libiconv) || flag?(:win32) || (flag?(:android) && LibC::ANDROID_API < 28) %}
+{% if flag?(:use_libiconv) || host_flag?(:use_libiconv) || flag?(:win32) || (flag?(:android) && LibC::ANDROID_API < 28) %}

We’re not baking in host flags into the compiler because it is intended to be portable between systems. And other environments might not be using libiconv from macports.

Of course, it’s understandable that you always want to use that flag on your system.
Maybe exporting CRYSTAL_OPTS in your environment config is the best solution for that. Bonus value is that it works for any compiler, you don’t need to build it yourself.

As @kojix2 mentioned, you can pass FLAGS="-Duse_libiconv" directly to make instead of setting CRYSTAL_OPTS: make test FLAGS=-Duse_libiconv

How does it fail?

I chatted with ChatGPT for about 15 minutes, and its suggestion was to add a wrapper to the Portfile.

The problem is that wrappers aren’t commonly used in the programming language category on MacPorts. I looked at portfiles of various languages like nim, vlang, odin, and clojure, and none of them seemed to use wrappers.

We are not familiar with MacPorts, so I thought it would be a good idea to talk to the MacPorts team directly to find out about best practice.

1 Like

I just found out about the BeagleBoard

1 Like