
وقتی mirror های معمول جواب نمیدن یک راه پایدارتر برای build گرفتن از پروژههای Flutter و Android
اگر این چند وقت درگیر build گرفتن از پروژههای Flutter یا Android بودی احتمالن این صحنه برات آشناست:
همهچیز ظاهرن درست تنظیم شده، mirror هم گذاشتی، حتی آدرس wrapper و repositoryها رو هم عوض کردی، ولی Gradle باز وسط کار میره سراغ یه URL دیگه و همهچیز همونجا متوقف میشه. گاهی با timeout، گاهی با پیدا نشدن plugin، گاهی هم با خطاهایی که در نگاه اول اصلاً معلوم نیست از کجا اومدن.
چیزی که خیلیها اول متوجهش نمیشن اینه که استفاده از mirror فقط به این خلاصه نمیشه که چند خط توی settings.gradle.kts یا build.gradle عوض کنی. روی کاغذ، این روشها درستن. حتی بعضی وقتها هم جواب میدن. ولی در عمل مخصوصا تو پروژههای جدید Flutter، همهی درخواستهای Gradle از یک مسیر رد نمیشن. یه بخش مربوط به dependencyهاست، یه بخش مربوط به pluginها، یه بخش مربوط به Gradle Wrapper، و تازه خود Flutter هم بعضی وقتها buildهای داخلی خودش رو وارد داستان میکنه. برای همین ممکنه تو همهچیز رو داخل پروژه درست تنظیم کرده باشی، اما باز هم بخشی از build از زیر دستت رد بشه و بره سراغ منبع اصلی یا یه مخزن دیگه.
اینجاست که تفاوت بین یک راهحل موقت و یک راهحل پایدارتر مشخص میشه.
برای استفاده از mirror، راههای مختلفی وجود داره. بعضیها فقط gradle-wrapper.properties رو تغییر میدن. بعضیها میرن سراغ pluginManagement و dependencyResolutionManagement داخل settings.gradle.kts. بعضیها هم repositoryهای build.gradle رو میبندن و فکر میکنن کار تموم شده. این روشها بد نیستن، ولی یه مشکل مشترک دارن: بیشترشون محدود به همون پروژهان. یعنی فقط همون بخشی رو کنترل میکنن که جلوی چشم تو قرار داره. اگر Gradle یا Flutter در لایهای پایینتر یا در یک build داخلی، repository دیگهای اضافه کنه، اون تنظیمها همیشه کافی نیستن.
به همین خاطر، توی تجربهی عملی، یکی از پایدارترین روشها استفاده از این فایل init.d\mirror.init.gradle.kts هست.
دلیلش هم خیلی پیچیده نیست. این فایل دیگه فقط تنظیم یک پروژه نیست بلکه خود Gradle رو در سطح سیستم تنظیم میکنی. یعنی هر repository که در جریان build اضافه بشه، از یک لایه بالاتر بررسی و در صورت نیاز rewrite میشه. به زبان سادهتر، بهجای اینکه امیدوار باشی همهی پروژهها و همهی buildهای داخلی دقیقاً همون repoهایی رو بخونن که تو داخل فایلهای پروژه گذاشتی، خود رفتار Gradle رو هدایت میکنی. همین باعث میشه این روش در سناریوهایی که plugin resolution، included buildها یا dependencyهای پنهان وسط کار وارد ماجرا میشن، معمولاً پایدارتر از بقیه روشها عمل کنه.
توی تجربهی من، بین mirror های ایرانی که برای توسعهی اندروید امتحان کردم، https://maven.myket.ir از کاملترین گزینهها بود و روی بعضی پروژهها که با mirrorهای دیگه build نمیشدن، نتیجهی بهتری داد. دقیقتر بگم، بعضی mirrorها برای dependencyهای معمولی خوب عمل میکردن، ولی وقتی build به pluginها، included buildهای Flutter یا artifactهای خاصتر میرسید، دوباره خطا برمیگشت. برای همین اگر هدفت اینه که یک راهحل عملی و نسبتا پایدار داشته باشی، مایکت فعلا یکی از گزینههای خوبه.
برای پیاده سازی این روش لازمه این فایل رو در مسیر زیر بسازی و این کدهارو داخلش کپی و سیو کنی:
مسیر کامل توی ویندوز:C:\Users\<username>\.gradle\init.d\mirror.init.gradle.kts
نام فایل mirror.init.gradle.kts <username>= نام کاربری شما
(اگر پوشه init.d وجود نداره بسازش)
محتوای فایل:
import org.gradle.api.artifacts.dsl.RepositoryHandler import org.gradle.api.artifacts.repositories.MavenArtifactRepository val MIRROR = "https://maven.myket.ir" val REDIRECT_HOSTS = setOf( "repo.maven.apache.org", "repo1.maven.org", "dl.google.com", "maven.google.com", "plugins.gradle.org", "jitpack.io", "jcenter.bintray.com", "maven.fabric.io", "developer.huawei.com", "storage.googleapis.com", ) fun shouldRedirect(url: String): Boolean = REDIRECT_HOSTS.any { host -> url.contains(host, ignoreCase = true) } fun interceptRepos(repos: RepositoryHandler) { repos.whenObjectAdded { val repo = this as? MavenArtifactRepository ?: return@whenObjectAdded val original = repo.url.toString() if (shouldRedirect(original)) { println("[Myket Mirror] Redirecting: $original -> $MIRROR") repo.setUrl(MIRROR) } } } gradle.beforeSettings { pluginManagement { interceptRepos(repositories) repositories { maven(url = MIRROR) } } @Suppress("UnstableApiUsage") dependencyResolutionManagement { interceptRepos(repositories) repositories { maven(url = MIRROR) { name = "myket-mirror" } } } } gradle.allprojects { buildscript { interceptRepos(repositories) repositories { clear() maven(url = MIRROR) } } interceptRepos(repositories) repositories.whenObjectAdded { val repo = this as? MavenArtifactRepository ?: return@whenObjectAdded val original = repo.url.toString() if (shouldRedirect(original)) { println("[Myket Mirror] Redirecting project repo: $original -> $MIRROR") repo.setUrl(MIRROR) } } }
فقط این نکته رو یادت باشه که این فایل، جای تنظیم wrapper رو نمیگیره. یعنی اگر خواستی دانلود خود Gradle هم از مایکت انجام بشه، باید gradle-wrapper.properties هم جداگانه تنظیم کنی:
مسیر فایل:
project_folder\android\gradle\wrapper\gradle-wrapper.properties
در این فایل به دنبال distributionUrl بگرد و با عبارت زیر جایگزینش کن:
distributionUrl=https\://maven.myket.ir/gradle/distributions/gradle-8.12-all.zip
و تمام!
البته اینجا باید روی یک نکته تأکید کرد: پایدارتر بودن به معنی بینقص بودن نیست. این روش فقط مسیر resolve شدن repositoryها رو بهتر کنترل میکنه. اگر mirror تو واقعاً artifact یا plugin لازم رو نداشته باشه، build باز هم fail میشه. init script قرار نیست چیزی رو از هیچ بسازه؛ فقط کمک میکنه درخواستها از مسیری عبور کنن که تو میخوای، نه از مسیری که Gradle بهصورت پیشفرض ترجیح میده. برای همین وقتی بعد از این تنظیم باز هم خطا میبینی، باید اول از همه این سؤال رو بپرسی: آیا هنوز درخواستها دارن به مقصد اشتباه میرن، یا این بار درست به mirror میرن ولی خود mirror پاسخ کامل نداره؟ همین فرق ظاهراً کوچک، توی عیبیابی خیلی تعیینکنندهست.
و در نهایت مهمترین نکتهی این مقاله شاید همین باشه:
بهترین حالت همچنان اینه که اصلن از mirror استفاده نکنی و مستقیم از منابع اصلی دانلود انجام بشه. این همیشه تمیزترین، قابلپیشبینیترین و کمدردسرترین حالت ممکنه. mirror هرچقدر هم خوب و سریع باشه، باز یک واسطه است. ممکنه cache شدن بعضی artifactها زمان ببره ممکنه بعضی dependencyها کامل پشتیبانی نشن یا ممکنه رفتارش در بعضی سناریوها دقیقن شبیه منبع اصلی نباشه. برای همین این راهکارها رو بهتره به چشم یک راهحل موقت و عملی برای شرایط خاص ببینی، نه نسخهی نهایی و ایدهآل برای همهی پروژهها.