Eskiden projelerde do.sh adında bir shell scripti kullanırdık. Makefile ile benzer işlevi görüyordu; ancak bu scriptte yeni fonksiyonlar yazmak, env değişkenlerini kullanmak gibi ekstra şeyler de yapıyorduk. Bütün fikir, README.md dosyasında neyi hangi komutlarla yapacağımızı belirtmek yerine, o komutları sırayla çalıştıran, komutların çalışması için gerekli kurulumları otomatik yapan veya sistem gereksinimlerini kontrol eden, çalıştırabilmek için sadece Bash’e ihtiyaç duyan bir script yazmaktı.

Sonra ben bunu DOSH adında Python projesine çevirdim. Altyapı olarak hazır, ihtiyaç oldukça geliştirmeye, yeni özellikler eklemeye devam ediyorum. Bash yerine Lua syntax’ini kullanarak benzer scriptler hazırlayabiliyoruz. Fakat DOSH ile birlikte birkaç yeni problem ortaya çıktı:

  1. Sıfır bağımlılık kuralı. Bash scripti olunca, MacOS ve Linux’ta (hatta WSL varsa Windows’ta), o scripti çalıştırmak için ekstra bir şey yüklemenize gerek yoktu. Bir Python uygulamasında bunu nasıl yapacağız?

  2. Sürekli geliştirme ihtiyacı. İhtiyaç hiç bitmiyor. yt-dlp‘de olduğu gibi herkesin özel bir amaç için bir yerinden tutup bir fonksiyon yazması gerekiyor ki projenin bir anlamı olsun. Örneğin ben blogumdaki resimlerin boyutunu küçültmek için tinypng kullanıyorum. Onun yerine dosh compress-images dediğimde, ya da deploy etmeden önce (dosh deploy-blog) önce otomatik olarak tüm imajların boyutunu sıkıştırabilmeyi isterdim.

İkinci maddeyi bir kenara bırakıp, konumuza dönelim. Bir Python projesinde sıfır bağımlılık kuralını nasıl sağlayabiliriz? Okumaya başlamadan önce başlıklara kabaca bir göz atmanızı öneririm.

Ne Yapmak İstiyoruz?

Kurulması kolay ve kullanabilmek için ek bir talimat gerektirmeyen bir uygulama yapmak istiyoruz. Yani uygulamayı çalıştırmak için python.org sitesine gidip Python indirmemeliyim. Biz buna self-executable diyoruz. Uygulamamız tek bir dosyadan ibaret olabilir, böylece indirmesi ve kurulması daha kolay olur; bir arşiv dosyası içinde birden fazla dosya ve dizinden de oluşabilir. İki yöntemin de birtakım avantajları ve dezavantajları var. Örneğin bizim ilk tercihimiz muhtemelen tek bir dosyadan ibaret uygulama dosyası indirmek olacak; ama komutu her çalıştırdığımızda o dosyayı parse etmesi, yorumlaması için zaman kaybedecek. Bir desktop uygulaması için bu zaman kayıpları belki gözardı edilebilir ama CLI uygulaması ise, milisaniyeler içinde de olsa başlangıçta beklemek bir noktadan sonra can sıkıcı olmaya başlayacak.

Bir kavram daha var, self-contained. Bu yöntemde, adından da anlaşılacağı üzere, sadece interpreter değil, bağımlılıklarının da paketin içinde tutulduğu; sadece programın başlatılması için değil, tüm fonksiyonları ile çalışabilmesi için asgari tüm gereksinimlere sahip olması kastediliyor. Arada ince ama net bir fark var. Tekrar edelim, bir Python uygulamasını başlatabilmenin yolu, bir Python interpreterine sahip olmak. Ama bir de programın bağımlılıkları var, örneğin arayüz için Qt kullanıyorsanız ve siz bilgisayarınıza Qt ve Qt Python bağlayıcılarını kurmadıysanız, uygulama eksik bağımlılık nedeniyle çalışmayacaktır.

“Ya madem interpreteri içine koydun, bağımlılıkları niye dahil etmez ki bir insan?” diye düşünebilirsiniz. Zaten Python’da paylaşımlı kitaplık (shared library) kavramı pek kullandığımız, ihtiyaç duyduğumuz bir şey değil. O daha çok C++, C#, Java gibi dillerde ihtiyaç olan bir durum. Belki denk gelmişsinizdir, bir oyun yüklersiniz Steam’den, sonra çalıştırmak istediğinizde “Microsoft Visual C++ 2015 Runtime” bla bla bileşenini yükleyin diye bir popup açılır ve oyun kapanır. O bir shared librarydır. Binlerce oyunun içine tek tek gömmek yerine siz onu bir kere yüklersiniz, bütün oyunlar artık aynı kitaplığı kullanarak çalışır. Bu gibi çok fazla ortak bileşen olunca oyun başına indirme maliyetini düşürmek açısından paylaşımlı kitaplıkları kullanmak mantıklıdır.

Şimdi yönteme karar verelim.

Hangi Yöntemi Tercih Etmeliyiz?

Elimizdeki seçenekleri bir gözden geçirelim. Bütün seçenekleri Python özelinde değerlendireceğiz:

İşletim Sistemlerinin Paket Yöneticilerini Kullanmak

Windows’ta WinGet, MacOS’te Homebrew, Linux’ta… Ubuntu için .deb paketi, Fedora için .rpm, Arch Linux için AUR’da PKGBUILD, veya tüm distrolar için Flatpak, ya da Snap… Yazarken yoruldum. Tek bir uygulama için bu kadar emek harcamaya değer mi? Belki, sadece MacOS desteklememiz yeterli olsaydı mantıklı. Veya çok sağlam bir komuniteniz varsa herkes bir yerinden tutup tamamlar. Ama bir de bunların güncellenmesi, bakımının yapılması, otomasyonu var. Ayrıca, başta belirttiğim sıfır bağımlılık kuralını birazcık ucundan çiğniyor. MacOS için Homebrew paketini hazırladınız; ama kullanıcı Homebrew kullanmıyorsa ne olacak? Belki MacPorts kullanıyor, onun da mı paketini yapmalı? Yaptınız diyelim, repoya kabul edilmesi kolay olacak mı, ne kadar zaman alacak?

Kısaca, mantıklı olduğu özel durumlar olabilir ama DOSH için hayır. Bu yöntem, self executable’a bir örnek.

Python Paket Yöneticilerini Kullanmak

Bazı uygulamalar amacı gereği bir Python interpreterinin sistemde kurulu olmasını gerektirebilir. Örneğin bir AI engineersiniz ve Anaconda kullanıyorsunuz. Benim uygulamam Anaconda kullanıcılarının kullanımına yönelik ise, bir conda paketi yapmak mantıklı olabilir. Diğer Python distrolarını desteklemek gerekirse PyPI’ya yüklemek daha mantıklı olabilir.

Ben DOSH’u dilden bağımsız bir uygulama olarak tasarladığım için bu yöntem benim işime yaramadı. Ancak tek bir istisnası var. O da astral-uv. Uv, diğer Python paket yöneticilerinin kısmen yaptığı her şeyi tek başına yapabiliyor. Yani Python interpreteri yüklemeden, hatta uygulamanızı da öncesinde kurdurmadan uygulamanızı denettirebilirsiniz. Sadece iki adım:

  1. Uv’u kur.
  2. Bu komutu çalıştır: uvx <uygulamanizin-komutu>

Oldukça basit, tek ihtiyacınız olan şey, uygulamanızın PyPI‘da olması. Bu yöntem ne bir self-executable, ne de self-contained örneği. Çünkü interpreter’i yüklüyoruz, ve onun paket yöneticisiyle bağımlılıklarını yüklemeye çalışıyoruz, veya sadece uv yüklüyoruz.

Zipapp

Açıkçası ben self-contained bir uygulamanın aynı zamanda self-executable olmasını bekliyorum ama zipapp biraz bunun dışında kalan bir yöntem. Bir interpreter kurulumuna ihtiyacınız var; ama hiçbir bağımlılık kurmanıza gerek yok. Çünkü zipapp sayesinde tüm bağımlılıkların içinde olduğu bir arşiv paketi oluşturuyorsunuz.

Bu ne zaman işe yarar? İlk aklıma gelenler:

  1. Şirket içinde kullandığınız, PyPI’ya gönderemediğiniz, ama CodeArtifact gibi registry sistemlerle uğraşmak istemediğinizde.
  2. Basit projeler için Docker containerization ile uğraşmak yerine sadece uygulamayı paketleyip deploy etmek istediğinizde.
  3. Veya on-premise bir servis için code obfuscation sürecinin bir parçası olarak kullanabilirsiniz. Ne kadar işe yarayabilir, araştırmak gerek.

Kısacası, bu yöntemin de mantıklı olduğu alanlar var; ama DOSH için hayır.

PyInstaller, Nuitka, PyApp, ve Diğerleri

Son seçenek olarak, sıfır bağımlılık ve kullanıma hazır paketler hazırlamak için yapılmış uygulamaları değerlendirelim. Niye en baştan bunlara bakmadık; çünkü hem göründükleri kadar kolay değiller, ekstra efor ve bakım gerektiriyor, hem de “there’s no silver bullet.”, yani diğer seçenekler gibi bunların da hiçbiri bir problemi tam olarak çözmüyor. Benim tavsiyem, mümkün mertebe diğer basit seçeneklerle sorunu çözmeye çalışmak, onlar işe yaramadığında PyInstaller gibi araçların sorunu çözeceğine kendimizi ikna etmek.

Bunların her birinin farklı bir yaklaşımı ve önceliği var. Tek dosya veya tek dizin haline getirebilirsiniz. Sadece self-executable haline getirip bağımlılıkların sonradan kurulmasını sağlayabilirsiniz. Veya tamamen offline çalışabilecek bir kurulum paketi haline getirebilirsiniz. Bunların bir kısmı executable path’e otomatik eklendiği için direkt komuta erişebilir hale geliyor; bir kısmı için dizini veya dosyayı doğru path’e taşımanız gerekebilir. Hatta bir kısmı da code obfuscation problemlerini kendisi çözebiliyor, eğer kodun gizliliği ile ilgili kaygınız varsa tercih sebebi olabilir.

Ama her seçeneğin bir dezavantajlı noktası var. Tek dosya yapınca performans düşüyor, sadece executable yaparsanız bağımlılıkları yüklemek için internete bağımlı oluyorsunuz vesaire.

Ben Nasıl Yaptım?

DOSH, zaten internete bağımlı bir uygulama olduğu için self-contained olmasını önemsemedim. Tek istediğim, hiç Python bilmeyen birisinin bile kurup kullanabileceği bir uygulama yapmak. PyApp CLI performansı ve oluşturulan paketin boyutu konusunda bana göre en iyisiydi ve onu tercih ettim. Önce, executable dosyanın oluşturulabilmesi için bir BASH scripti yazdım:

export PYAPP_PROJECT_NAME="dosh"
export PYAPP_PROJECT_PATH="$(find $(pwd)/${{ inputs.dist-path }} -name "dosh*.whl" -type f | head -1)"

echo "Packaging DOSH binary: ${{ inputs.dosh-binary-name }}"
echo "Using wheel: ${PYAPP_PROJECT_PATH}"

# download pyapp source code, build it
curl https://github.com/ofek/pyapp/releases/latest/download/source.tar.gz -Lo pyapp-source.tar.gz
tar -xzf pyapp-source.tar.gz
mv pyapp-v* pyapp-latest
cd pyapp-latest
cargo build --release

# and rename the binary
mkdir -p ../${{ inputs.output-path }}
mv target/release/pyapp "../${{ inputs.output-path }}/${{ inputs.dosh-binary-name }}"
chmod +x "../${{ inputs.output-path }}/${{ inputs.dosh-binary-name }}"

echo "Binary packaged successfully at ${{ inputs.output-path }}/${{ inputs.dosh-binary-name }}"

Sonra, bu scripti GitHub Actions’ta, her yeni sürüm çıkardığımda otomatik olarak Linux (x86_64, arm64), MacOS (Apple Silicon), Windows (x86_64) platformlarında çalıştırmak için action olarak tanımladım. Workflow’un tamamını buradan bulabilirsiniz, sadece Linux’tan örnek gösteriyorum:

  package-on-linux-arm64:
    runs-on: ubuntu-24.04-arm

    needs:
      - build

    steps:
      - uses: actions/checkout@v4

      - uses: ./.github/actions/package-dosh
        with:
          dosh-binary-name: dosh-linux-arm64
          dist-path: dist
          output-path: bin

Ben her sürüm tagı eklediğimde bu workflow çalışacak, binaryleri oluşturacak ve bu dosyaları artifact olarak saklayacak. Bu arada binary boyutlarına aldanmayın, PyApp ile bir uygulamayı paketlediğinizde, uygulamayı ilk çalıştırırken bootstrap ediyor, dolayısıyla en azından ilk kez çalıştırmak için internete bağımlılığınız var. Ama benim için kabul edilebilir bir dezavantaj:

Bir sonraki adım olarak, bu build edilmiş executable dosyaların son kullanıcının kullanımına açık olması için GitHub Release’da yayımlayacağız:

      - uses: ncipollo/release-action@v1
        with:
          artifacts: "dosh-*/dosh-*,release-dists/*"

Artık uygulama, son kullanıcıya açık! İndirip kullanabilirler. Bu bir CLI uygulama olduğu için, biraz daha pratiklik sağlamak için install.sh ve install.ps1 scriptleri oluşturdum. Böylece README.md dosyasında, kullanıcılara terminalden yükleme yapmalarını tavsiye ediyorum:

sh <(curl https://raw.githubusercontent.com/gkmngrgn/dosh/main/install.sh)

Windows kullanıcıları için:

iwr https://raw.githubusercontent.com/gkmngrgn/dosh/main/install.ps1 -useb | iex

Aynı workflow’da ikinci bir seçenek olarak, paketi PyPI’ya gönderiyorum. Basit ve yedek bir kurulum alternatifi olarak kullanıcıya pip veya uv ile kurma şansı bırakıyorum:

      - name: Publish release distributions to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          packages-dir: release-dists/
$ alias dosh="uvx --from dosh-cli dosh"
$ dosh

Artık yapısal bir değişiklik olmadığı müddetçe, bir daha paketleme için efor sarfetmeme gerek kalmadı.

Son Söz

Paketleme yönteminizi seçerken, eğer kapalı kaynak kodlu bir proje geliştiriyorsanız, lisansları ve code obfuscation konusunu da göz önünde bulundurmanız gerekiyor. Umarım bu sorunla uğraşanlar için yol gösterici olmuştur.

DOSH henüz bitmiş veya kullanıma hazır bir proje değil; ancak ilginizi çekerse katkılarınızı bekliyorum. Yorumlarınız için bana sosyal medya veya email üzerinden ulaşabilirsiniz.