以 Docker 容器編譯並執行 Objective-C 程式

    前言

    由於 Objective-C 在非蘋果平台上不是主流語言,不一定每個 GNU/Linux 發行版都會有預編好的 Objective-C 編譯器和 GNUstep 函式庫。編譯 Objective-C 編譯器 (GCC 或 Clang) 和 GNUstep 都是複雜且耗時的任務,難以自行完成。

    因應這項議題,本文介紹以 Docker 編譯 Objective-C 程式的方式。此外,由於 Docker Hub 中所分享的 GNUstep 映像檔都很舊了,本文會從 Ubuntu 基底映像檔重新建立 GNUstep 開發環境。

    前置作業

    使用系統的套件管理程式來安裝 Docker 即可。此處以 openSUSE 為例:

    $ sudo zypper install docker
    

    裝完後要啟動 Docker 服務:

    $ sudo systemctl start docker
    

    如果很常用 Docker 的話,可自行將 Docker 服務加入開機啟動程式中。

    在命令列上直接執行 Docker 容器的方式

    我們在先前的文章已經提過了,此處不再重覆。

    下載 Ubuntu 映像檔

    由於 Docker Hub 上現存的 GNUsetp 映像檔都有點年代了,這裡只下載 Ubuntu 基礎映像檔來用:

    $ sudo docker pull ubuntu:20.04
    

    這個映像檔對應到 Ubuntu 20.04 LTS,這是最新版本的長期支援版本,應該可以用好一陣子。

    在 Docker 容器中建立 GNUstep 開發環境

    使用 -it 參數即可以互動模式進入 Docker 容器:

    $ sudo docker run -it ubuntu:20.04 /bin/bash
    

    在 Docker 環境中會自動提升至 root,所以可以直接安裝軟體。參考以下指令來建置 GNUstep 開發環境:

    # apt update
    # apt upgrade
    # apt install gcc g++ gobjc gobjc++ clang gnustep-devel
    # exit
    

    將更動過的 Docker 容器提交 (commit) 到新的映像檔:

    $ sudo docker ps -l -q
    28a2937bd585
    $ sudo docker commit 28a2937bd585 gnustep
    

    每次的行程號碼都不一樣,請不要照抄。建議在提交映像檔時另起一個新的映像檔名稱,像是本例的 gnustep,原本的 Ubuntu 映像檔可以留著建置其他的開發環境。

    以 GCC 編譯和執行 Objective-C 容器

    由於執行 Docker 容器的指令很長,每次都手動執行不太經濟,這裡將該指令包裝成 shell 命令稿 $HOME/bin/objc

    #!/usr/bin/sh
    
    input=$1
    
    if ! [ -f $input ];
    then
        sudo docker run --rm --cap-drop=all gnustep /bin/sh -c "gcc $@"
        exit $?
    fi
    
    shift;  # Consume the parameter.
    
    gcc_command="gcc -o a.out ${input} -lobjc -lgnustep-base "
    gcc_command+="-I /usr/include/GNUstep -L /usr/lib/GNUstep "
    gcc_command+="-fobjc-exceptions -fconstant-string-class=NSConstantString"
    
    sudo docker run --rm -w /app --cap-drop=all -v `pwd`:/app gnustep /bin/sh -c \
    "cp /app/$input /tmp; cd /tmp;"\
    "${gcc_command};"\
    "export LD_LIBRARY_PATH=/usr/lib/GNUstep; ./a.out $@"
    

    注意 GNUstep 在 Ubuntu 上的路徑並非系統標準路徑,所以要設置額外的參數。

    雖然 Objective-C++ 相對少見,我們也為其寫了一個 shell 命令稿 $HOME/bin/objc++

    #!/usr/bin/sh
    
    input=$1
    
    if ! [ -f $input ];
    then
        sudo docker run --rm --cap-drop=all gnustep /bin/sh -c "g++ $@"
        exit $?
    fi
    
    shift;  # Consume the parameter.
    
    gcc_command="g++ -o a.out ${input} -lobjc -lgnustep-base "
    gcc_command+="-I /usr/include/GNUstep -L /usr/lib/GNUstep "
    gcc_command+="-fobjc-exceptions -fconstant-string-class=NSConstantString"
    
    sudo docker run --rm -w /app --cap-drop=all -v `pwd`:/app gnustep /bin/sh -c \
    "cp /app/$input /tmp; cd /tmp;"\
    "${gcc_command};"\
    "export LD_LIBRARY_PATH=/usr/lib/GNUstep; ./a.out $@"
    

    其實只是把 gcc 改為 g++ 而已。

    使用方式如下:

    $ objc path/to/source.m
    

    以 Clang 編譯和執行 Objective-C 程式

    目前 Objective-C 比較新的語法特性只在 Clang 上有實作,所以本節略為修改先前的 shell 命令稿,改用 clang(1) 來編譯程式。

    為了要和 GNUstep 相容,這裡刻意使用 GCC 內附的 libobjc。為了取得 libobjc 的標頭檔路徑,先執行單次 Docker 指令以取得該映像檔內的 GCC 函式庫路徑:

    $ sudo docker run gnustep /bin/sh -c "gcc -print-prog-name=cc1 /dev/null"
    /usr/lib/gcc/x86_64-linux-gnu/9/cc1
    

    由此可知 GCC 函式庫的標頭檔位於 /usr/lib/gcc/x86_64-linux-gnu/9/include 。只要映像檔沒換,這個路徑是固定的,直接寫死在命令稿內也無妨。

    接著修改 objc 命令稿:

    #!/usr/bin/sh
    
    input=$1
    
    if ! [ -f $input ];
    then
        sudo docker run --rm --cap-drop=all gnustep /bin/sh -c "clang $@"
        exit $?
    fi
    
    shift;  # Consume the parameter.
    
    gcc_command="clang -o a.out ${input} -lobjc -lgnustep-base "
    gcc_command+="-I /usr/include/GNUstep -L /usr/lib/GNUstep "
    gcc_command+="-fobjc-exceptions -fconstant-string-class=NSConstantString"
    
    sudo docker run --rm -w /app --cap-drop=all -v `pwd`:/app gnustep /bin/sh -c \
    "cp /app/$input /tmp; cd /tmp;"\
    "${gcc_command} -I /usr/lib/gcc/x86_64-linux-gnu/9/include;"\
    "export LD_LIBRARY_PATH=/usr/lib/GNUstep; ./a.out $@"
    

    用同樣的概念修改 objc++ 命令稿:

    #!/usr/bin/sh
    
    input=$1
    
    if ! [ -f $input ];
    then
        sudo docker run --rm --cap-drop=all gnustep /bin/sh -c "clang++ $@"
        exit $?
    fi
    
    shift;  # Consume the parameter.
    
    gcc_command="clang++ -o a.out ${input} -lobjc -lgnustep-base "
    gcc_command+="-I /usr/include/GNUstep -L /usr/lib/GNUstep "
    gcc_command+="-fobjc-exceptions -fconstant-string-class=NSConstantString"
    
    sudo docker run --rm -w /app --cap-drop=all -v `pwd`:/app gnustep /bin/sh -c \
    "cp /app/$input /tmp; cd /tmp;"\
    "${gcc_command} -I /usr/lib/gcc/x86_64-linux-gnu/9/include;"\
    "export LD_LIBRARY_PATH=/usr/lib/GNUstep; ./a.out $@"
    

    這時候就可以用 Clang 來編譯 Objective-C 程式。

    注意事項

    本文所提供的 shell 命令稿以內部使用為主。由於本文所提供的命令稿皆未考慮使用 Docker 的安全事項,勿將這些命令稿用於對外公開的服務程式。

    本方案的限制

    本方案所用的 shell 命令稿只能用在單一 Objective-C 程式碼,而且該 Objective-C 程式無法讀入外部檔案。如果需要更複雜的使用情境,就要自行修改命令稿。

    對於有多個檔案的 Objective-C 專案來說,另外寫 Dockerfile 是比較好的選擇。限於篇幅,不在本文說明撰寫 Dockerfile 的方式。

    由於 Clang 和 GCC 對 Objective-C 程式碼不相容,請依專案需求自行選擇所需的 shell 命令稿。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Yahoo
    【追蹤本站】
    Facebook Facebook Twitter