OSX ऐप बंडल का निर्माण


90

मान लीजिए कि मैंने Xcode का उपयोग किए बिना एक OSX ऐप बनाया है। जीसीसी के साथ संकलित करने के बाद मुझे एक निष्पादन योग्य मिलता है जो कई अन्य पुस्तकालयों से जुड़ा हुआ है। उन पुस्तकालयों में से कुछ को फिर से अन्य गैर-मानक प्रणाली पुस्तकालयों से गतिशील रूप से जोड़ा जा सकता है

क्या कोई ऐसा उपकरण मौजूद है जो आवश्यक निर्देशिका संरचनाओं को पहले बनाकर OSX ऐप बंडल बनाता है और फिर यह सुनिश्चित करने के लिए कि सभी डायनेमिक निर्भरताएँ ऐप बंडल में भी हैं, लिंक की नकल / जाँच / फिक्सिंग करते हैं?

मुझे लगता है कि मैं ऐसा कुछ लिखने की कोशिश कर सकता हूं, लेकिन मैं सोच रहा था कि क्या ऐसा कुछ पहले से मौजूद है।


4
मैं कई कारणों की वजह से Xcode का उपयोग नहीं कर सकता। जिनमें से एक है क्योंकि मैं कस्टम जीसीसी का उपयोग करता हूं। Xcode मुझे एक अलग gcc निर्दिष्ट करने की अनुमति नहीं देता है। मैं अपने मेकअप का निर्माण करने के लिए cmake का उपयोग कर रहा हूं।
योगी

>> उन पुस्तकालयों में से कुछ को फिर से अन्य गैर-मानक प्रणाली पुस्तकालयों से गतिशील रूप से जोड़ा जा सकता है << चयनित प्रतिक्रिया इस मामले में मदद नहीं करती है। आपने इसे कैसे हल किया?
फ्लोयू। SimpleUITesting.com

जवाबों:


143

MacOSX, ईजी और अग्ली पर ऐप बंडल बनाने के दो तरीके हैं।

आसान तरीका सिर्फ XCode का उपयोग करना है। किया हुआ।

समस्या यह है कि कभी-कभी आप ऐसा नहीं कर सकते।

मेरे मामले में मैं एक ऐप बना रहा हूं जो अन्य ऐप बनाता है। मैं यह नहीं मान सकता कि उपयोगकर्ता के पास XCode स्थापित है। मैं अपने ऐप पर निर्भर पुस्तकालयों के निर्माण के लिए MacPorts का भी उपयोग कर रहा हूं । मुझे यह सुनिश्चित करने की आवश्यकता है कि इन डिलेबियों को मैं वितरित करने से पहले ऐप के साथ बंडल कर दूं।

अस्वीकरण: मैं इस पोस्ट को लिखने के लिए पूरी तरह से अयोग्य हूँ, इसमें मौजूद सभी चीज़ों को ऐप्पल डॉक्स से अलग किया गया है, मौजूदा एप्लिकेशन और परीक्षण और त्रुटि को छोड़कर। यह मेरे लिए काम करता है, लेकिन सबसे अधिक गलत है। यदि आपके पास कोई सुधार है तो कृपया मुझे ईमेल करें।

सबसे पहले आपको पता होना चाहिए कि एक ऐप बंडल केवल एक निर्देशिका है।
आइए एक काल्पनिक foo.app की संरचना की जांच करें।

foo.app/
    सामग्री /
        Info.plist
        मैक ओ एस/
            foo
        संसाधन /
            foo.icns

Info.plist एक सादा XML फ़ाइल है। आप इसे एक पाठ संपादक या संपत्ति सूची संपादक ऐप के साथ संपादित कर सकते हैं जो XCode के साथ बंडल में आता है। (यह / डेवलपर / अनुप्रयोग / उपयोगिताएँ / निर्देशिका में है)।

आपको जिन महत्वपूर्ण चीजों को शामिल करना है वे हैं:

CFBundleName - ऐप का नाम।

CFBundleIcon - एक आइकन फ़ाइल जिसमें सामग्री / संसाधन dir है। आइकन बनाने के लिए आइकन संगीतकार ऐप का उपयोग करें। (यह / डेवलपर / एप्लिकेशन / यूटिलिटीज / डायरेक्टरी में भी है) आप इसे केवल खिड़की पर एक पीएनजी खींच सकते हैं और छोड़ सकते हैं और स्वचालित रूप से आपके लिए एमआईपी-स्तर उत्पन्न करना चाहिए।

CFBundleExecutable - निष्पादन योग्य फ़ाइल का नाम सामग्री / MacOS / उप-फ़ोल्डर में माना जाता है।

बहुत अधिक विकल्प हैं, जो ऊपर सूचीबद्ध हैं वे केवल नंगे न्यूनतम हैं। यहाँ Info.plist फ़ाइल और ऐप बंडल संरचना पर कुछ Apple प्रलेखन हैं ।

इसके अलावा, यहाँ एक नमूना Info.plist है।

<? xml संस्करण = "1.0" एन्कोडिंग = "UTF-8"?>
<! DOCTYPE plist सार्वजनिक "- // Apple कंप्यूटर // DTD PLIST 1.0 // EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist संस्करण = "1.0">
<Dict>
  <कुंजी> CFBundleGetInfoString </ कुंजी>
  <String> फू </ स्ट्रिंग>
  <कुंजी> CFBundleExecutable </ कुंजी>
  <String> foo </ स्ट्रिंग>
  <कुंजी> CFBundleIdentifier </ कुंजी>
  <String> com.your-कंपनी-name.www </ स्ट्रिंग>
  <कुंजी> CFBundleName </ कुंजी>
  <String> foo </ स्ट्रिंग>
  <कुंजी> CFBundleIconFile </ कुंजी>
  <String> foo.icns </ स्ट्रिंग>
  <कुंजी> CFBundleShortVersionString </ कुंजी>
  <String> 0.01 </ स्ट्रिंग>
  <कुंजी> CFBundleInfoDictionaryVersion </ कुंजी>
  <String> 6.0 </ स्ट्रिंग>
  <कुंजी> CFBundlePackageType </ कुंजी>
  <String> एपीपीएल </ स्ट्रिंग>
  <कुंजी> IFMajorVersion </ कुंजी>
  <पूर्णांक> 0 </ पूर्णांक>
  <कुंजी> IFMinorVersion </ कुंजी>
  <पूर्णांक> 1 </ पूर्णांक>
</ Dict>
</ Plist>

एक आदर्श दुनिया में आप बस अपने निष्पादन योग्य सामग्री / MacOS / dir में छोड़ सकते हैं और किया जा सकता है। हालाँकि, यदि आपके ऐप में कोई गैर-मानक dylib निर्भरता है तो यह काम नहीं करेगा। विंडोज की तरह, मैकओएस यह डीएलएल हेल का अपना विशेष प्रकार है ।

यदि आप उन पुस्तकालयों के निर्माण के लिए MacPorts का उपयोग कर रहे हैं , जिनके खिलाफ आप लिंक करते हैं, तो dylibs के स्थान आपके निष्पादन योग्य में हार्ड-कोड किए जाएंगे। यदि आप एक ऐसी मशीन पर ऐप चलाते हैं जिसमें ठीक उसी स्थान पर dylibs हैं, तो यह ठीक चलेगा। हालाँकि, अधिकांश उपयोगकर्ताओं ने उन्हें स्थापित नहीं किया होगा; जब वे आपके ऐप को डबल-क्लिक करेंगे तो यह दुर्घटनाग्रस्त हो जाएगा।

इससे पहले कि आप आपको निष्पादन योग्य वितरित करें, आपको उन सभी dylibs को इकट्ठा करना होगा जो इसे लोड करते हैं और उन्हें ऐप बंडल में कॉपी करते हैं। आपको निष्पादन योग्य को संपादित करने की भी आवश्यकता होगी ताकि यह सही जगह पर dylibs के लिए दिखेगा। यानी जहां आपने उन्हें कॉपी किया है।

एक संपादन योग्य हाथ खतरनाक लगता है? सौभाग्य से मदद करने के लिए कमांड लाइन उपकरण हैं।

ओटूल-एल निष्पादन योग्य_नाम

यह कमांड उन सभी dylibs को सूचीबद्ध करेगा जो आपका ऐप निर्भर करता है। यदि आपको कोई ऐसा सिस्टम या लाइब्रेरी / usr / lib फ़ोल्डर दिखाई नहीं देता है, तो वे हैं जिन्हें आपको ऐप बंडल में कॉपी करना होगा। उन्हें / सामग्री / MacOS / फ़ोल्डर में कॉपी करें। अगला आपको नई dylibs का उपयोग करने के लिए निष्पादन योग्य को संपादित करने की आवश्यकता होगी।

सबसे पहले, आपको यह सुनिश्चित करने की आवश्यकता है कि आप -headerpad_max_install_names ध्वज का उपयोग करके लिंक करें। यह सिर्फ यह सुनिश्चित करता है कि यदि नया डाइलिब पथ लंबा है, तो पहले वाला, इसके लिए जगह होगी।

दूसरा, प्रत्येक dylib पथ को बदलने के लिए install_name_tool का उपयोग करें।

install_name_tool -change मौजूदा_पठ_तो_भार्य @ निष्पादन योग्य_पथ / ब्लाह.डिबलिब निष्पादन योग्य_नाम

एक व्यावहारिक उदाहरण के रूप में, मान लीजिए कि आपका ऐप libSDL का उपयोग करता है , और ओटोल सूचियां "/opt/local/lib/libSDL-1.2.0.dylib" के रूप में इसका स्थान है।

सबसे पहले इसे एप बंडल में कॉपी करें।

cp /opt/local/lib/libSDL-1.2.0.dylib foo.app/Contents/MacOS/

फिर नए स्थान का उपयोग करने के लिए निष्पादन योग्य को संपादित करें (नोट: सुनिश्चित करें कि आपने इसे -headerpad_max_install_ames ध्वज के साथ बनाया है)

स्थापित करें

वाह, हम लगभग पूरा कर चुके हैं। अब वर्तमान कार्यशील निर्देशिका के साथ एक छोटा मुद्दा है।

जब आप अपना ऐप शुरू करते हैं तो वर्तमान निर्देशिका उस निर्देशिका के ऊपर होगी जहां एप्लिकेशन स्थित है। उदाहरण के लिए: यदि आप foo.app को / Applcations फ़ोल्डर में रखते हैं तो वर्तमान निर्देशिका जब आप एप्लिकेशन लॉन्च करेंगे / एप्लिकेशन फ़ोल्डर होगा। जैसा कि आप उम्मीद कर सकते हैं /Applications/foo.app/Contents/MacOS/ नहीं।

आप इसके लिए अपने ऐप को बदल सकते हैं, या आप इस मैजिक लिटिल लॉन्चर स्क्रिप्ट का उपयोग कर सकते हैं, जो वर्तमान डायरेक्टरी को बदल देगी और आपके ऐप को लॉन्च करेगी।

#! / Bin / bash
सीडी "$ {0% / *}"
./foo

सुनिश्चित करें कि आप Info.plist फ़ाइल को समायोजित करते हैं ताकि CFBundleExecutable लॉन्च स्क्रिप्ट की ओर इशारा करता है और पिछले निष्पादन योग्य के लिए नहीं।

ठीक है, अब सब किया। सौभाग्य से, एक बार जब आप यह सब सामान जानते हैं तो आप इसे एक निर्माण स्क्रिप्ट में दफन कर देते हैं।


7
+1। हालांकि, ज़ोंबी कुत्ते ने मुझे बाहर निकाल दिया। क्या आप DLL नरक का वर्णन करने के लिए कम डरावनी तस्वीर का उपयोग कर सकते हैं? (यह उल्लेख नहीं है कि "डीएलएल नरक" शब्द लागू नहीं होता है। गतिशील पुस्तकालयों को वितरित करने के लिए पसंदीदा तरीका चौखटों के माध्यम से है जो अधिकांश समस्याओं से बचता है। फिर भी, .dylib का दृष्टिकोण UNIX- शैली के पुस्तकालयों के लिए उपयोगी है।)
हेनिन एपफेल्मस

1
हम दोहरी निर्भरता को कैसे ठीक करते हैं? एक dylib की तरह एक और dylib का संदर्भ?
फ्लोयू। SimpleUITesting.com

क्या निष्पादन योग्य को सही स्थानों के साथ संकलित करने का कोई तरीका नहीं है, इसलिए आपको इसे संपादित करने की आवश्यकता नहीं है?
सिंक्रोनाइजर

क्या यह कैटालिना पर भी काम करेगा, यह देखते हुए कि सुरक्षा कितनी कड़ी हो गई है? (मॉडिफाइंग डायलिब्स, बायनेरिज़ इत्यादि)
सिंक्रनाइज़र

20

मुझे वास्तव में एक बहुत ही उपयोगी उपकरण मिला जो कुछ श्रेय का हकदार है ... नहीं - मैंने इसे विकसित नहीं किया है;)

https://github.com/auriamg/macdylibbundler/

यह आपके एप्लिकेशन बंडल में सुचारू रूप से काम करने के लिए आपकी निर्भरता के साथ-साथ आपकी dylib फ़ाइलों को "ठीक" करने के लिए सभी निर्भरता को हल करेगा।

... यह आपके आश्रित गतिशील कामों की निर्भरता के लिए भी जाँच करेगा: डी


यह एक बहुत उपयोगी उपकरण है! इसे साझा करने और इसे विकसित करने के लिए धन्यवाद।
xpnimi

यह यहाँ निर्भरता की निर्भरता गायब है। ठीक कर?
पीटर

9

मैं अपने मेकफाइल में इसका उपयोग करता हूं ... यह एक ऐप बंडल बनाता है। इसे पढ़ें और इसे समझें, क्योंकि आपको PkgInfo और Info.plist फ़ाइलों के साथ एक macosx / फ़ोल्डर में एक png आइकन फ़ाइल की आवश्यकता होगी, जिसमें मैं यहाँ शामिल हूँ ...

"यह मेरे कंप्यूटर पर काम करता है" ... मैं Mavericks पर कई ऐप्स के लिए इसका उपयोग करता हूं ...

APPNAME=MyApp
APPBUNDLE=$(APPNAME).app
APPBUNDLECONTENTS=$(APPBUNDLE)/Contents
APPBUNDLEEXE=$(APPBUNDLECONTENTS)/MacOS
APPBUNDLERESOURCES=$(APPBUNDLECONTENTS)/Resources
APPBUNDLEICON=$(APPBUNDLECONTENTS)/Resources
appbundle: macosx/$(APPNAME).icns
    rm -rf $(APPBUNDLE)
    mkdir $(APPBUNDLE)
    mkdir $(APPBUNDLE)/Contents
    mkdir $(APPBUNDLE)/Contents/MacOS
    mkdir $(APPBUNDLE)/Contents/Resources
    cp macosx/Info.plist $(APPBUNDLECONTENTS)/
    cp macosx/PkgInfo $(APPBUNDLECONTENTS)/
    cp macosx/$(APPNAME).icns $(APPBUNDLEICON)/
    cp $(OUTFILE) $(APPBUNDLEEXE)/$(APPNAME)

macosx/$(APPNAME).icns: macosx/$(APPNAME)Icon.png
    rm -rf macosx/$(APPNAME).iconset
    mkdir macosx/$(APPNAME).iconset
    sips -z 16 16     macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_16x16.png
    sips -z 32 32     macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_16x16@2x.png
    sips -z 32 32     macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_32x32.png
    sips -z 64 64     macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_32x32@2x.png
    sips -z 128 128   macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_128x128.png
    sips -z 256 256   macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_128x128@2x.png
    sips -z 256 256   macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_256x256.png
    sips -z 512 512   macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_256x256@2x.png
    sips -z 512 512   macosx/$(APPNAME)Icon.png --out macosx/$(APPNAME).iconset/icon_512x512.png
    cp macosx/$(APPNAME)Icon.png macosx/$(APPNAME).iconset/icon_512x512@2x.png
    iconutil -c icns -o macosx/$(APPNAME).icns macosx/$(APPNAME).iconset
    rm -r macosx/$(APPNAME).iconset

Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>English</string>
    <key>CFBundleExecutable</key>
    <string>MyApp</string>
    <key>CFBundleGetInfoString</key>
    <string>0.48.2, Copyright 2013 my company</string>
    <key>CFBundleIconFile</key>
    <string>MyApp.icns</string>
    <key>CFBundleIdentifier</key>
    <string>com.mycompany.MyApp</string>
    <key>CFBundleDocumentTypes</key>
    <array>
    </array>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>0.48.2</string>
    <key>CFBundleSignature</key>
    <string>MyAp</string>
    <key>CFBundleVersion</key>
    <string>0.48.2</string>
    <key>NSHumanReadableCopyright</key>
    <string>Copyright 2013 my company.</string>
    <key>LSMinimumSystemVersion</key>
    <string>10.3</string>
</dict>
</plist>

PkgInfo

APPLMyAp

आप Info.plist को टेक्स्ट प्लेसहोल्डर बना सकते हैं और फिर अंतिम फ़ील्ड बनाने के लिए sed या awk जैसी किसी चीज़ का उपयोग कर सकते हैं। या हो सकता है कि प्लिस्ट बनाने के लिए प्लूटिल का भी इस्तेमाल करें।
मार्क हीथ

8

सबसे सरल उपाय है: एक बार कुछ भी बदले बिना एक Xcode प्रोजेक्ट बनाएं (यानी Xcode आपके लिए जो सरल वन-विंडो ऐप है) उसे बनाए रखें, उसका निर्माण करें और आपके लिए बनाए गए बंडल को कॉपी करें। फिर, अपनी सामग्री के अनुरूप फ़ाइलों को संपादित करें (विशेष रूप से Info.plist), और सामग्री / MacOS / निर्देशिका में अपना बाइनरी डालें।


4
प्रश्न स्पष्ट रूप से पूछता है कि यह बिना xcode के कैसे किया जाए। मैं एक ही नाव में हूं और एक वास्तविक उत्तर चाहूंगा।
हाइपरलॉजिक जूल

5
खैर, इसके साथ, आपको अपने जीवन में केवल एक बार XCode का उपयोग करना होगा :) या किसी को आपके लिए करने के लिए कहना होगा।
F'x

@ F'x क्या आप केवल एक उदाहरण के रूप में उस .app को प्रकाशित कर सकते हैं?
एंडोलिथ

3

विशिष्ट वातावरण के लिए आश्रित पुस्तकालयों के साथ ऐप बंडल बनाने में मदद करने के लिए कुछ खुले स्रोत उपकरण हैं, उदाहरण के लिए, पायथन-आधारित अनुप्रयोगों के लिए py2app । यदि आपको अधिक सामान्य नहीं मिलता है, तो शायद आप इसे अपनी आवश्यकताओं के अनुसार ढाल सकते हैं।


2

काश मुझे यह पोस्ट पहले मिली हो ...।

यहां एक Run scriptचरण का उपयोग करके इस समस्या को हल करने का मेरा संक्षिप्त तरीका है जो हर बार Releaseमेरे ऐप के एक संस्करण का निर्माण करने पर लागू होता है :

# this is an array of my dependencies' libraries paths 
# which will be iterated in order to find those dependencies using otool -L
libpaths=("$NDNRTC_LIB_PATH" "$BOOST_LIB_PATH" "$NDNCHAT_LIB_PATH" "$NDNCPP_LIB_PATH" "/opt/local/lib")
frameworksDir=$BUILT_PRODUCTS_DIR/$FRAMEWORKS_FOLDER_PATH
executable=$BUILT_PRODUCTS_DIR/$EXECUTABLE_PATH

#echo "libpaths $libpaths"
bRecursion=0
lRecursion=0

# this function iterates through libpaths array
# and checks binary with "otool -L" command for containment
# of dependency which has "libpath" path
# if such dependency has been found, it will be copied to Frameworks 
# folder and binary will be fixed with "install_name_tool -change" command
# to point to Frameworks/<dependency> library
# then, dependency is checked recursively with resolveDependencies function
function resolveDependencies()
{
    local binfile=$1
    local prefix=$2
    local binname=$(basename $binfile)
    local offset=$((lRecursion*20))
    printf "%s :\t%s\n" $prefix "resolving $binname..."

    for path in ${libpaths[@]}; do
        local temp=$path
        #echo "check lib path $path"
        local pattern="$path/([A-z0-9.-]+\.dylib)"
        while [[ "$(otool -L ${binfile})" =~ $pattern ]]; do
            local libname=${BASH_REMATCH[1]}
            otool -L ${binfile}
            #echo "found match $libname"
            printf "%s :\t%s\n" $prefix "fixing $libname..."
            local libpath="${path}/$libname"
            #echo "cp $libpath $frameworksDir"
            ${SRCROOT}/sudocp.sh $libpath $frameworksDir/$libname $(whoami)
            local installLibPath="@rpath/$libname"
            #echo "install_name_tool -change $libpath $installLibPath $binfile"
            if [ "$libname" == "$binname" ]; then
                install_name_tool -id "@rpath/$libname" $binfile
                printf "%s :\t%s\n" $prefix "fixed id for $libname."
            else
                install_name_tool -change $libpath $installLibPath $binfile
                printf "%s :\t%s\n" $prefix "$libname dependency resolved."
                let lRecursion++
                resolveDependencies "$frameworksDir/$libname" "$prefix>$libname"
                resolveBoostDependencies "$frameworksDir/$libname" "$prefix>$libname"
                let lRecursion--
            fi
            path=$temp
        done # while
    done # for

    printf "%s :\t%s\n" $prefix "$(basename $binfile) resolved."
} # resolveDependencies

# for some reason, unlike other dependencies which maintain full path
# in "otool -L" output, boost libraries do not - they just appear 
# as "libboost_xxxx.dylib" entries, without fully qualified path
# thus, resolveDependencies can't be used and a designated function is needed
# this function works pretty much in a similar way to resolveDependencies
# but targets only dependencies starting with "libboost_", copies them
# to the Frameworks folder and resolves them recursively
function resolveBoostDependencies()
{
    local binfile=$1
    local prefix=$2
    local binname=$(basename $binfile)
    local offset=$(((bRecursion+lRecursion)*20))
    printf "%s :\t%s\n" $prefix "resolving Boost for $(basename $binfile)..."

    local pattern="[[:space:]]libboost_([A-z0-9.-]+\.dylib)"
    while [[ "$(otool -L ${binfile})" =~ $pattern ]]; do
        local libname="libboost_${BASH_REMATCH[1]}"
        #echo "found match $libname"
        local libpath="${BOOST_LIB_PATH}/$libname"
        #echo "cp $libpath $frameworksDir"
        ${SRCROOT}/sudocp.sh $libpath $frameworksDir/$libname $(whoami)
        installLibPath="@rpath/$libname"
        #echo "install_name_tool -change $libname $installLibPath $binfile"
        if [ "$libname" == "$binname" ]; then
            install_name_tool -id "@rpath/$libname" $binfile
            printf "%s :\t%s\n" $prefix "fixed id for $libname."
        else
            install_name_tool -change $libname $installLibPath $binfile
            printf "%s :\t%s\n" $prefix "$libname Boost dependency resolved."
            let bRecursion++
            resolveBoostDependencies "$frameworksDir/$libname" "$prefix>$libname"
            let bRecursion--
        fi
    done # while

    printf "%s :\t%s\n" $prefix "$(basename $binfile) resolved."
}

resolveDependencies $executable $(basename $executable)
resolveBoostDependencies $executable $(basename $executable)

आशा है कि यह किसी के लिए उपयोगी हो सकता है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.