NSIS使用,打包C#項目


NSIS打包WPF項目

NSIS版本

NSIS_v251_Build_20160601 - NSISFANS.zip

NSIS使用

我是使用HM VNISEdit2.0.3生成初步腳本,再編輯腳本增加其他功能。

新建腳本:向導

打開HM VNISEdit2.0.3。點擊頂部菜單欄的文件,選擇新建腳本:向導

第1步(1/8)

聲明一些應用程序信息,沒什么好說的。

第2步(2/8)

安裝程序語言,用戶圖形界面,壓縮算法我都是默認的,一般就只會改一下圖標和文件名。

第3步(3/8)

從來沒改動過,默認就行。

第4步(4/8)

這里如果沒有授權文件,可以直接為空,不填就可以了。

第5步(5/8)

首先③是默認的2條信息,我一般直接刪除。

從①處添加單獨的文件,從②處添加文件夾文件。

一般先通過②添加整個項目文件。

注意紅框內的選擇。

如果需要創建快捷方式,再通過①添加項目中的exe文件。

最終選擇如下:

第6步(6/8)

默認即可。

第7步(7/8)

默認即可

第8步(8/8)

默認即可

至此,向導結束,生成腳本。總結來說,只有第5步,選擇項目文件的時候需要注意一下,其他大部分都默認即可。

通過編輯腳本,增加一些功能

安裝前先卸載之前版本

Function .onInit
  !insertmacro MUI_LANGDLL_DISPLAY
  ReadRegStr $0 HKLM "${PRODUCT_UNINST_KEY}" "UninstallString"  
  
  ${If} $0 != ""  
    MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "系統需移除之前版本 ${PRODUCT_NAME} 后繼續安裝,確定移除之前版本?" IDNO +4
    System::Call 'kernel32::GetModuleFileNameA(i 0, t .R0, i 1024) i r1'
    WriteRegStr HKLM "${PRODUCT_UNINST_KEY}" "InstallString" "$R0"
    Exec "$0"
    Abort
  ${EndIf} 
FunctionEnd

若卸載時軟件正在運行,則終止卸載並提示關閉軟件

FindProcDLL::FindProc "Client.exe"
Pop $R0
IntCmp $R0 1 0 no_run
MessageBox MB_ICONSTOP "卸載程序檢測到 ${PRODUCT_NAME} 正在運行,請關閉之后再卸載!"
Quit

附錄:NSIS用戶手冊

摘錄自https://www.nsisfans.com/help/index.html。其中一些我用到的和我以為能用到的知識點。

NSIS腳本

腳本格式

命令

命令行的格式為: '命令 [參數]'

File "myfile"

注釋

3種注釋方式。“;xxx”、“# xxx”、“/* xxx */”

; 第1種注釋方式

# 第2種注釋方式

/* 
第3種注釋方式
*/

插件

要調用一個插件,使用 '插件::命令 [參數]'。

;調用插件nsExec的命令Exec,參數是“我的文件”。應該是這個意思
nsExec::Exec "我的文件"

數字

十進制:正常

十六進制:0x123

八進制:0678

; 注意顏色被設置為十六進制 RGB 形式,像 HTML 那樣但是沒有 # 開頭。
SetCtlColors $HWND CCCCCC

字符串

MessageBox MB_OK "I'll be happy" ; 把一個 ' 放在字符串里
MessageBox MB_OK 'And he said to me "Hi there!"' ; 把一個 " 放在字符串里
MessageBox MB_OK `And he said to me "I'll be happy!"` ; 這里把 ' 和 " 都放到了字符串里
MessageBox MB_OK "$\"A quote from a wise man$\" said the wise man" ; 這里演示了跳過引號的解析。說實話沒懂啥意思,跳過啥了?$\"代表雙引號?

變量

變量以 $ 開頭。用戶變量應該 (不是必須) 事先被聲明並且區分大小寫。

Var MYVAR
StrCpy $MYVAR "變量值"

長命令

要把命令擴充為多行,需要在行尾使用反斜杠 \ ,下一行會被自動連接到上一行的尾部。

也可用於注釋,容易混淆,不建議使用

例如:

CreateShortCut "$SMPROGRAMS\NSIS\ZIP2EXE project workspace.lnk" \
    "$INSTDIR\source\zip2exe\zip2exe.dsw"

MessageBox MB_YESNO|MB_ICONQUESTION \
    "是否要刪除文件夾中的所有文件? \
    (如果你想保留任何你自己創建的文件 \
     請點擊[否])" \
    IDNO NoRemoveLabel

# 一個注釋\
    在這里仍然是注釋...

配置文件

如果在 NSIS 的配置目錄下存在 "nsisconf.nsh" ,那么它里面的代碼默認將會被包含在任何腳本里(除非你使用了 /NOCONFIG 命令參數)。在 Window 平台下配置目錄就是 makensis.exe 所在的目錄,其它平台下根據安裝時的設置有所不同而默認為 $PREFIX/etc/ 。你可以在運行時替換它。

沒懂啥意思

變量

所有的變量都是全局的並且可以用於區段和函數。需要注意的是,在默認情況下變量被限制在 1024字節。要擴大這個限制你需要重新構建一個使用了更大 NSIS_MAX_STRLEN值的 NSIS 或使用特別版本。

這個變量是啥?和上面那個!define定義的“符號”有啥區別?

用戶變量

$VARNAME

用戶變量可以用Var命令來聲明。你可以使用這些變量來保存值,用於字符串操作等等。

Var

語法:Var [/GLOBAL] 變量名

聲明一個用戶變量。變量名允許的字符: [a-z] [A-Z] [0-9] 和 '_'。 所有定義的變量都是全局的,即使在區段或函數內定義。 要使它表達更清楚一些,區段或函數內定義的變量必須使用 /GLOBAL 標記。在區段和函數之外不需要 /GLOBAL 標記。

;定義用戶變量;區段外的,自然是全局
Var example

Function testVar
  ;定義用戶變量;區段內的,必須使用/GLOBAL來標識它也是全局的
  Var /GLOBAL example2
  ;使用用戶變量
  StrCpy $example "example value"
  StrCpy $example2 "another example value"
FunctionEnd

其他可寫的變量

$0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $R0, $R1, $R2, $R3, $R4, $R5, $R6, $R7, $R8, $R9

寄存器。這些變量可以像用戶變量一樣使用,但常用於公用函數或宏。你不需要聲明這些變量,所以當你在公用代碼里使用他們的時候不能有任何名字沖突。當在公用代碼里使用這些變量的時候,推薦你使用堆棧保存和恢復他們原來的數據。這些變量也可以在插件里傳遞,因為他們可以被 DLL 插件讀取和寫入。

$INSTDIR

安裝目錄 ($INSTDIR 可以使用 StrCpy、 ReadRegStr、 ReadINIStr 等等來更改。例如在 .onInit 函數里可以用來做高級的檢測安裝定位)。

注意在卸載程序代碼里,$INSTDIR 為卸載程序所在的目錄而不是在安裝程序里所指定的目錄。例如, 如果你把卸載程序放在 $WINDIR 里並且用戶沒有移動它,那么在卸載程序里 $INSTDIR 就等於 $WINDIR。如果你要把卸載程序放到另外的位置,那么你應該先把安裝程序的 $INSTDIR 值寫入注冊表或者其它容易保存的地方,然后在卸載程序里讀取該值並賦值給卸載程序里的 $INSTDIR

$OUTDIR

當前輸出目錄 (通過 SetOutPath 或者通過 StrCpy、 ReadRegStr、 ReadINIStr 等等來修改)

$CMDLINE

安裝程序命令行。命令行的格式可以是下列之一:

  • "完整路徑\安裝程序.exe" 參數1 參數2 參數3
  • 安裝程序.exe 參數1 參數2 參數3
  • 對於解析“參數”部分,參閱 GetParameters。如果在命令行里指定了 /D= (用來跳過安裝路徑的選擇),那么 /D= 后面的參數將不會被保存在 $CMDLINE (前面的可以保存)。
$LANGUAGE

當前使用的語言標識符。例如,英語是 1033。你可以在 .onInit 里更改此變量。

常量

常量通常用在 InstallDir屬性里。

需要注意的是一些新的常量並不是在所有的 OS上都是正常的。例如 $CDBURN_AREA 僅在 Windows XP及以上系統中才正常。如果在 Windows 98 中使用將會得到空值。除非特別提示,否則該常量都是在所有 OS上有效的。

一些常用常量
  • $PROGRAMFILES:程序文件目錄(通常為 C:\Program Files但是運行時會檢測)。
  • $COMMONFILES:公用文件目錄。這是應用程序共享組件的目錄(通常為 C:\Program Files\Common Files但是運行時會檢測)。
  • $DESKTOP:Windows 桌面目錄(通常為 C:\windows\desktop但是運行時會檢測)。該常量的內容(所有用戶或當前用戶)取決於 SetShellVarContext設置。默認為當前用戶。
  • $EXEDIR:安裝程序運行時的位置。(從技術上來說你可以修改改變量,但並不是一個好方法)。
  • ${NSISDIR}:包含 NSIS 安裝目錄的一個標記。在編譯時會檢測到。常用於在你想調用一個在 NSIS 目錄下的資源時,例如:圖標、界面……
  • $WINDIR:Windows 目錄(通常為 C:\windows或 C:\winnt 但在運行時會檢測)
  • $SYSDIR:Windows 系統目錄(通常為 C:\windows\system或 C:\winnt\system32 但在運行時會檢測)
  • $TEMP:系統臨時目錄(通常為 C:\windows\temp但在運行時會檢測)
  • $STARTMENU:開始菜單目錄(常用於添加一個開始菜單項,使用 CreateShortCut)。該常量的內容(所有用戶或當前用戶)取決於 SetShellVarContext 設置。默認為當前用戶。
  • $SMPROGRAMS:開始菜單程序目錄(當你想定位 $STARTMENU\程序時可以使用它)。該常量的內容(所有用戶或當前用戶)取決於 SetShellVarContext 設置。默認為當前用戶。
  • $SMSTARTUP:開始菜單程序/啟動目錄。該常量的內容(所有用戶或當前用戶)取決於 SetShellVarContext設置。默認為當前用戶。
  • $QUICKLAUNCH:在 IE4 活動桌面及以上的快速啟動目錄。如果快速啟動不可用,僅僅返回和 $TEMP一樣。
  • $DOCUMENTS:文檔目錄。一個當前用戶典型的路徑形如 C:\Documents and Settings\Foo\My Documents。這個常量的內容(所有用戶或當前用戶)取決於 SetShellVarContext 設置。默認為當前用戶。該常量在 Windows 95且 Internet Explorer 4 沒有安裝時無效。
  • $SENDTO:該目錄包含了“發送到”菜單快捷項。
  • $RECENT:該目錄包含了指向用戶最近文檔的快捷方式。
  • $FAVORITES:該目錄包含了指向用戶網絡收藏夾、文檔等的快捷方式。這個常量的內容(所有用戶或當前用戶)取決於 SetShellVarContext設置。默認為當前用戶。該常量在 Windows 95 且 Internet Explorer 4沒有安裝時無效。
  • $MUSIC:用戶的音樂文件目錄。這個常量的內容(所有用戶或當前用戶)取決於 SetShellVarContext設置。默認為當前用戶。該常量僅在 Windows XP、ME及以上才有效。
  • $PICTURES:用戶的圖片目錄。這個常量的內容(所有用戶或當前用戶)取決於 SetShellVarContext設置。默認為當前用戶。該常量僅在 Windows 2000、XP、ME及以上才有效。
  • $VIDEOS:用戶的視頻文件目錄。這個常量的內容(所有用戶或當前用戶)取決於 SetShellVarContext設置。默認為當前用戶。該常量僅在 Windows XP、ME及以上才有效。
  • $NETHOOD:該目錄包含了可能存在於我的網絡位置、網上鄰居文件夾的鏈接對象。該常量在 Windows 95且 Internet Explorer 4 和活動桌面沒有安裝時無效。
  • $FONTS:系統字體目錄。
  • $TEMPLATES:文檔模板目錄。這個常量的內容(所有用戶或當前用戶)取決於 SetShellVarContext設置。默認為當前用戶。
  • $APPDATA:應用程序數據目錄。當前用戶路徑的檢測需要 Internet Explorer 4及以上。所有用戶路徑的檢測需要 Internet Explorer 5 及以上。這個常量的內容(所有用戶或當前用戶)取決於 SetShellVarContext設置。默認為當前用戶。該常量在 Windows 95 且 Internet Explorer 4和活動桌面沒有安裝時無效。
  • $LOCALAPPDATA:本機應用程序數據目錄。該常量僅在 Windows 2000或以上系統有效。.
  • $PRINTHOOD:該目錄包含了可能存在於打印機文件夾的鏈接對象。該常量在 Windows 95和 Windows 98 上無效。
  • $INTERNET_CACHE:Internet Explorer的臨時文件目錄。該常量在 Windows 95 和 Windows NT且 Internet Explorer 4 和活動桌面沒有安裝時無效。
  • $COOKIES:Internet Explorer的 Cookies 目錄。該常量在 Windows 95和 Windows NT 且 Internet Explorer 4和活動桌面沒有安裝時無效。
  • $HISTORY:Internet Explorer的歷史記錄目錄。該常量在 Windows 95 和 Windows NT且 Internet Explorer 4 和活動桌面沒有安裝時無效。
  • $PROFILE:用戶的個人配置目錄。一個典型的路徑如 C:\Documents and Settings\Foo。該常量在 Windows 2000及以上有效。
  • $ADMINTOOLS:一個保存管理工具的目錄。這個常量的內容(所有用戶或當前用戶)取決於 SetShellVarContext設置。默認為當前用戶。該常量在 Windows 2000、ME及以上有效。
  • $RESOURCES:該資源目錄保存了主題和其他 Windows資源(通常為 C:\Windows\Resources但在運行時會檢測)。該常量在 Windows XP及以上有效。
  • $RESOURCES_LOCALIZED:該本地的資源目錄保存了主題和其他 Windows資源(通常為 C:\Windows\Resources\1033但在運行時會檢測)。該常量在 Windows XP及以上有效。
  • $CDBURN_AREA:一個在燒錄 CD 時儲存文件的目錄。該常量在 Windows XP 及以上有效。
  • $HWNDPARENT:父窗口的十進制窗口句柄。
  • $PLUGINSDIR:該路徑是一個臨時目錄,當第一次使用一個插件或一個調用 InitPluginsDir時被創建。該文件夾當安裝程序退出時會被自動刪除。這個文件夾的用意是用來保存給 InstallOptions使用的 INI 文件、啟動畫面位圖或其他插件運行需要的文件。
字符串里的常量
  • $$:轉義,用來表示 $。
  • $\r:用來表示一個回車(\r)。
  • $\n:用來表示新的一行(\n)。
  • $\t:用來表示一個 Tab(\t)。

跳轉

標記

標記是 Goto 指令的目標,

或各種分支指令 (例如 IfErrors、 MessageBox、 IfFileExists, 和 StrCmp) 的目標。

標記必須存在於一個區段或函數里。標記是局限於該范圍里的,這意味着這些指令只能訪問和它們同處於一個區段或函數的標記。

聲明標記,不能以 -, +, !, $, 或 0-9開頭。 當給多個需要指定標記的指令指定了標記,不要忘了使用空字串 ("") 或 0 來表示下一個指令(表示無 Goto 跳轉)。一個標記以句點 (.) 開頭時表示這是一個全局的標記,你可以從任何區段或函數直接跳轉到它上面(但是你不能從一個安裝程序跳轉到一個卸載程序的全局標記,反之亦然)。

聲明標記很簡單

MyLabel:

Goto MyLabel

相對跳轉

和標記不一樣,相對跳轉就如其名一樣,相對於被調用的地方進行跳轉。在任何你可用到標記的地方你也都可以相對跳轉。

相對跳轉由數字標定。+1 跳轉到下一條指令(默認的步進),+2 會跳過1條指令也並且從當前指令轉到第2條指令,-2 將往后跳2條指令,+10 將會跳過 9 條指令,從當前指令跳到第10條指令。

一條指令就是在安裝程序運行時可以被執行的所有命令。

MessageBox、 Goto、 GetDLLVersion、 FileRead、 SetShellVarContext 此類是指令.

AddSize、 Section、 SectionGroup、 SectionEnd、 SetOverwrite (和所有處於編譯器標記)、 Name、 SetFont、 LangString 都不是指令,因為它們在編譯時就被執行。

;例子1,很簡單
Goto +2
MessageBox MB_OK "你將看不到這個消息框"
MessageBox MB_OK "上一條信息被略過,這條信息才會被顯示"

;例子2,這里是會顯示 "隨后的這條信息將會被略過" 這句嗎?沒給答案額
;Goto +4 的結果是 Goto -3
Goto +4
MessageBox MB_OK "隨后的這條信息將會被略過"
Goto +3
MessageBox MB_OK "你將不會看到這個消息框"
Goto -3
MessageBox MB_OK "完成"

需要注意的是 宏指令 並不是真正意義上的一條指令,在編譯時會被展開為若干條指令,所以你不能用相對跳轉來跳過一條宏指令。下面的示例演示了相對跳轉並沒有真正跳過插入的宏 (僅僅是跳過了宏里的第一條指令),還是會顯示一個消息框。

!macro relative_jump_test
  MessageBox MB_OK "第一個宏行first macro line"
  MessageBox MB_OK "第二個宏行second macro line"
!macroend

Goto +2
!insertmacro relative_jump_test
;這里應該是顯示 "第二個宏行second macro line" 這句

頁面

這里用腳本向導生成的腳本,頁面都是調用的宏。本身對安裝,卸載也沒啥要求,默認就行。暫時不需要改動。

區段

每一個 NSIS 安裝程序包含一個或多個區段。所有的這些區段都使用下面的這些命令來創建、修改和結束。

  • 每一個區段包含零個或多個指令。
  • 區段是安裝程序執行的實體,並且如果設置了 ComponentText ,那么用戶就可以選擇禁止或允許每一個可見的區段。
  • 如果一個區段名為 'Uninstall' 或以 'un.' 為前綴,那么它就是一個卸載程序區段。

區段命令

AddSize

大小(單位為:KB)

告訴安裝程序當前的區段需要一個額外的 "大小" KB 磁盤空間。僅在一個區段里有效(在區段外或函數里無效)。

Section SectionEnd

語法:Section [/o] [([!]|[-])區段名] [區段索引輸出] xxx SectionEnd

Section 開始並且打開一個新的區段。

如果區段名為空、遺漏或者以一個 -開頭,那么它將是一個隱藏的區段,用戶也不能選擇禁止它。

如果一個區段名為“Uninstall”或以“un.”為前綴,那么它就是一個卸載程序區段。

如果指定了區段索引輸出,該參數將被定義 (!defined)為區段索引(然后可以對它使用 SectionSetText等)。

如果區段名以一個 !開頭,那么該區段的顯示名稱將以粗體字顯示。如果指定了 /o 開關,則該區段默認為不選。

SectionEnd 關閉當前打開的區段。

Section "-隱藏區段"
SectionEnd

Section # 隱藏區段
SectionEnd

Section "!描黑區段"
SectionEnd

Section /o "可選區段"
SectionEnd

Section "MainXX" SEC_IDX
SectionEnd

若需訪問區段索引,必需使用花括號括起來,且區段索引代碼必需在區段之后。

Section test1 sec1_id
SectionEnd

Section test2 sec2_id
SectionEnd

Function .onInit
  SectionGetText ${sec2_id} $0
  MessageBox MB_OK "索引 ${sec2_id} 的名稱:$\n$0" # 將正確顯示 '索引 1 的名稱: test2'
FunctionEnd
Function .onInit
  SectionGetText ${sec2_id} $0
  MessageBox MB_OK "索引 ${sec2_id} 的名稱:$\n$0" # 將錯誤顯示 '索引 ${sec2_id} 的名稱: test1'
    # 加上一個警告說明:
    #   未知 變量/常量 "{sec2_id}" 查看, 忽略
FunctionEnd

;我其實是沒太理解為啥后面又有一樣的一段。可能是為了介紹這句:“區段索引代碼必需在區段之后”。說明放在后面不對?
;Section test1 sec1_id
;SectionEnd

;Section test2 sec2_id
;SectionEnd
SectionIn
SectionGroup SectionGroupEnd

這兩感覺也用不上,有個印象。要是有用再看。

卸載區段

一個特別的名為 “Uninstall” 的區段只能是被創建用於產生一個卸載程序。該區段應該用來從系統里移除由安裝程序安裝的所有文件、注冊表健等等。下面是一個簡單的卸載區段例子:

;自動生成的腳本是沒有雙引號的 Section Uninstall。不知道是都行還是哪個錯了。有空試試
Section "Uninstall"
  Delete $INSTDIR\Uninst.exe ; 刪除自我 (看下面的解釋為什么可以這樣)
  Delete $INSTDIR\myApp.exe
  RMDir $INSTDIR
  DeleteRegKey HKLM SOFTWARE\myApp
SectionEnd

第一個 Delete指令是可以正常執行的 (刪除卸載程序本身),因為執行卸載程序的時候它會復制一個副本到系統臨時目錄並執行副本來完成卸載。

需要注意的是在卸載程序的代碼里, $INSTDIR 包含了卸載程序所在的位置。你不需要在安裝程序里給該變量重復賦值。

函數

函數類似於區段因為他們可以包含零個或多個指令。用戶函數不會被安裝程序直接調用,而必須在區段里使用 Call指令來調用。而當一個必然事件發生時回調函數將由安裝程序調用。

函數必須在區段或其他函數之外聲明。

函數命令

Function FunctionEnd

語法:Function [函數名] xxxx FunctionEnd

開始並打開一個新的函數。

一般函數名以 .開頭的(例如 ".Whatever")作為回調函數保留。函數名以 un.開頭的函數將會被創建在卸載程序里。因此,普通安裝區段和函數不能調用卸載函數,而卸載區段和卸載函數也不能調用普通安裝程序的函數。

Function myfunc
  # 一些命令
FunctionEnd

Section
  Call myfunc
SectionEnd

回調函數

安裝回調
.onInit

該回調將會在當安裝程序接近完成初始化時調用。如果在 '.onInit' 函數調用了 Abort,則安裝程序立即退出。

 Function .onInit
   MessageBox MB_YESNO "即將安裝。繼續?" IDYES NoAbort
   Abort ; 使得安裝程序退出。
   NoAbort:
 FunctionEnd

 Function .onInit
   ReadINIStr $INSTDIR $WINDIR\wincmd.ini Configuration InstallDir
   StrCmp $INSTDIR "" 0 NoAbort
   MessageBox MB_OK "未找到 Windows Commander 。未能取得安裝路徑。"
   Abort ; 使得安裝程序退出。
   NoAbort:
 FunctionEnd

這寫詢問框是什么個語法結構。

卸載回調
un.onInit

該回調將會在當卸載程序接近完成初始化時調用。如果 'un.onInit' 函數使用了 Abort ,則卸載程序立即退出。注意如果需要時該函數可以驗證和(或)修改 $INSTDIR 。

Function un.onInit
    MessageBox MB_YESNO "即將卸載。繼續?" IDYES NoAbort
    Abort ;使得卸載程序退出。
    NoAbort:
FunctionEnd

Function un.onInit
    IfFileExists $INSTDIR\myfile.exe found
    Messagebox MB_OK "卸載路徑不正確"
    Abort
    found:
FunctionEnd

安裝程序屬性

常規屬性

Name

語法:Name名稱 [雙與名稱](Name不區分大小寫,Name,name都可以)

設置安裝程序的名稱。名稱通常用來顯示產品的名稱比如“我的程序””。如果在名稱里有一個或多個與符號(&),把第二個參數設為與第一個相同,有 &的地方使用兩個 &來表示。例如你的產品名稱為“foo & bar2000”,那么使用:

Name "foo & bar2000" "foo && bar2000"

如果你的安裝程序名稱里有 & 符號並且對名稱使用了一個 LangString,那么你還需要創建另一個含有雙 &字符的名稱作為第二個參數。

接受變量。如果使用了變量,使用的變量必須在 .onInit被初始化。

;正常起名就1個參數就可以,但如果你的名字里有“&”,你需要2個參數,第二個參數將你名字種的“&”變為"&&"
;正常名字
Name "MyName"
;帶有"&"的名字
Name "MyName&YourName" "MyName&&YourName"
;帶參數的名字
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"

還有一種使用了LangString的名字。LangString什么意思?

LangString和多語言使用相關,如:LangString INFO1 ${LANG_ENGLISH} " Prompt message 1"。估計不太用得到,這里就不看了。

OutFile

語法:OutFile [路徑\]安裝程序.exe(也不區分大小寫)

指定 MakeNSIS 要寫入安裝程序的輸出文件。僅僅是 MakeNSIS 要寫入的文件,它不會對安裝程序的內容有任何影響。

就是腳本編譯后生成的那個安裝包的名字。

OutFile "Setup.exe"
InstallDir

設定安裝包默認的安裝目錄。

查看常量一節得到可用於該字串的變量(尤其是 $PROGRAMFILES)。注意該字串中最后一個 \后面的部分會被保留,當用戶在安裝時選擇“瀏覽”並取得新的目錄字串時,該部分會自動附加在后面(要禁止該行為你需要在目錄字串后面添加一個額外的 \,但是你要使用引號把目錄字串括起來以避免被認為是斷行處理)。如果沒有起作用的話,還是使用瀏覽按鈕吧。

InstallDir "$PROGRAMFILES\signjing安裝示例"
InstallDirRegKey

根鍵子鍵鍵名該屬性讓安裝程序去檢測一注冊表里的一個字串,如果該字串可用那么把它用來作為安裝目錄。如果預置了該屬性,當指定的注冊表鍵可用時它會越過 InstallDir指定的字串,否則使用默認的 InstallDir 指定值。

查詢注冊表時,該命令將自動截去引號(如 "C:\program files\poop" "%1",可得到 C:\program files\poop )。

如果該字串以 .exe結尾,它還會自動移去字串里的文件名部分(如 C:\program files\poop\poop.exe,將得到 C:\program files\poop )。

對於更多高級的安裝目錄配置,可在 .onInit函數里設定 $INSTDIR。

[譯者注:事實上它還可以自動截取如 "C:\program files\poop\poop.exe" "%1"而得到 C:\program files\poop ,這樣你只要知道了某個程序關聯的文件類型,就可以直接用它獲得該程序的安裝目錄。比如 InstallDirRegKey HKCR "FlashGet.Document\shell\open\command" "" 即可獲得網際快車的安裝目錄]

說了一大堆就沒怎么看懂

這個根鍵子鍵下面還有,我也不知道是根據子鍵打錯了,還是什么專有名詞

在網上找的另一個解釋,感覺更容易理解一點。好像就是通過注冊表獲取安裝路徑。

InstallDirRegKey只從注冊表中讀取,它從不寫入。 在執行.onInit之前,NSIS會執行以下操作:

  1. 如果設置了InstallDir,則該路徑將復制到$Instdir
  2. 如果設置了InstallDirRegKey並且存在注冊表項,則注冊表中的路徑(刪除文件名)將復制到$Instdir

因此,如果您希望InstallDirRegKey在下次用戶運行安裝程序時產生任何影響,您必須將其指向安裝程序在您的某個部分中創建的密鑰。它可以是卸載密鑰中的UninstallString命令,也可以是HKLKM \Software\My Company\My App等特定於應用程序的密鑰。 這一點的重點是,當用戶重新安裝或安裝新版本的應用程序時,它將安裝在同一文件夾中(覆蓋/升級現有安裝)。

;大概。沒找到詳細的語法結構:
InstallDirRegKey HKLM "${PRODUCT_UNINST_KEY}" "UninstallString"
ShowInstDetails

語法:ShowInstDetails hide|show|nevershow

設置是否顯示安裝詳細信息。你可以設為 hide來隱藏詳細信息但用戶可以查看,show 用來默認顯示詳細信息,nevershow可以阻止用戶查看任何信息。注意區段里可以使用 SetDetailsView 來更改它的設置。

ShowUninstDetails

語法:showUnInstDetails hide|show|nevershow

設置是否顯示卸載詳細信息。你可以設為 hide來隱藏詳細信息但用戶可以查看,show 用來默認顯示詳細信息,nevershow可以阻止用戶查看任何信息。注意區段里可以使用 SetDetailsView 來更改它的設置。

BrandingText

語法:BrandingText Context/TRIM(LEFT|RIGHT|CENTER)

文本設置顯示在安裝程序窗口底部的文本(默認為“Nullsoft Install System vX.XX”)。如果設為空字串("")則使用默認值;設為" "(一個空格)則顯示空白。如果你不知道哪個適合你,那就保留默認值吧,這樣可以使每個人知道你使用的 NSIS版本。使用 /TRIMLEFT、/TRIMRIGHT或 /TRIMCENTER 來裁剪控制大小到字串大小。

編譯器標記

SetOverwrite

語法:SetOverwrite on|off|try|ifnewer|ifdiff|lastused

該命令設置了由 File 命令使用的覆蓋標記來決定目標文件已存在時是否覆蓋。如果覆蓋標記為 on ,則目標文件被覆蓋(這個是默認值)。如果覆蓋標記為 off ,則已存在的文件不會被覆蓋。如果覆蓋標記為 try ,文件僅當可以被覆蓋時(就是說假如文件不能寫入,它會自動略過而不需要用戶決定)才會覆蓋目標文件。如果覆蓋標記為 ifnewer,則僅當已存在的文件比新文件舊時才會覆蓋目標文件。如果覆蓋標記為 ifdiff ,則僅當已存在的文件比新文件舊或新時才會覆蓋目標文件。注意在 ifnewer 或 ifdiff模式下,目標文件的日期都會被設為新文件的日期,而不管 SetDateSave 是怎么設置的。

版本信息

指令

NSIS 用於腳本的這些指令稍微的近似於 PHP 和匯編。它們沒有真正的高級語言結構,但是他們的指令(大多數情況下)卻是高級的,並且你可以很容易的掌握(比如你不用擔心字串的連接等等)。基本上你有 25 寄存器 (20 個常規用途, 5個特殊用途),和一個堆棧。

不知道這個“基本上你有 25 寄存器 (20 個常規用途, 5個特殊用途),和一個堆棧”這句具體啥意思。這怎么地也不止25個啊。是只有25差不過常用的?

基本指令

Delete

語法:Delete [/REBOOTOK] 文件

從目標系統刪除文件(可以是文件或通配符,但必須指定一個完整的路徑)。

如果指定了 /REBOOTOK並且該文件當前不可刪除,則會在系統重啟時刪除該文件 -- 如果該文件要在系統重啟時刪除,你還要設置一個重啟的標記。如果找到的文件不能被刪除則會置一個錯誤標記。但該錯誤標記不是為嘗試刪除一個不存在的文件設置的。

RMDir

語法:RMDir [/r] [/REBOOTOK] 目錄名

刪除指定的目錄(完整路徑)。

沒有 /r參數時只有在目錄為空時才會被刪除。如果指定了 /r,則目錄會被遞歸刪除,所有在指定目錄下的所有文件和目錄均被刪除。如果指定了 /REBOOTOK,任何當前不能刪除的文件或目錄將會在重啟后被刪除——如果文件或目錄需要在重啟時被刪除,會放置一個重啟的標記。當文件或目錄不能被刪除時放置一個錯誤的標記。

RMDir $INSTDIR
RMDir $INSTDIR\data
RMDir /r /REBOOTOK $INSTIDR
RMDir /REBOOTOK $INSTDIR\DLLs

;需要注意的是當前的工作目錄不能刪除。當前的工作目錄由 SetOutPath設定。例如,下面的例子里將不能刪除該目錄。
SetOutPath $TEMP\dir
RMDir $TEMP\dir
;而下面的例子可以刪除該目錄。
SetOutPath $TEMP\dir
SetOutPath $TEMP
RMDir $TEMP\dir

警告: 在卸載程序中使用 RMDir /r $INSTDIR 是不安全的。雖然用戶不太可能會選擇將程序安裝到 Program Files 文件夾中,但假如這樣的話,這個命令將會遞歸刪除整個 Program Files 文件夾,包括其他和卸載程序沒有任何關系的程序(目錄)。用戶還可以安裝除了程序文件之外的其他文件並希望它們能被卸載程序刪除。可用的解決方案可以輕松地實現只卸載由安裝程序釋放的唯一文件。

DeleteRegKey

語法:DeleteRegKey [/ifempty] 根鍵 子鍵

刪除一個注冊表鍵。

如果指定了 /ifempty,則該注冊表鍵僅當它無子鍵時才會被刪除(否則,整個注冊表鍵將被刪除)。有效的根鍵值在后面的 WriteRegStr列出。如果該鍵不能被刪除(或如果它不存在)則會放置一個錯誤的標記。

;HKLM:根鍵 , 子健:"Software\My Company\My Software"
DeleteRegKey HKLM "Software\My Company\My Software"
SetOutPath

設置輸出路徑($OUTFIR)且當路徑不存在時創建(需要時會遞歸創建)。必須為全路徑名,通常都使用 $INSTDIR

File

語法:File [/nonfatal] [/a] ([/r] [/x文件|通配符 [...]] (文件|通配符) [...] | /oname=輸出路徑\文件名輸入路徑\文件名)

我理解就是需要進行打包的所有文件。而且這明明更像輸入路徑。翻譯成“釋放文件”好別扭。感覺提取文件到輸出路徑更貼切一點。

釋放文件到當前輸出路徑($OUTDIR)。

  • 注意輸出文件名是 $OUTDIR\文件名。

  • 如果使用了 /oname=X 開關,則輸出文件會變為 $OUTDIR\X。當使用了 /oname=開關時只能指定一個文件,且輸出的文件名可以使用變量(或完整路徑如 $SYSDIR\whatever.dll)。如果輸出名稱包含了空格,你需要用雙引號把參數括起來,包括 /oname,就像下面例子顯示的那樣。

  • 支持通配符。

  • 如果使用了 /r ,匹配的文件將會在子目錄里被遞歸的搜索。如果目錄名匹配則所有包含的內容都會被遞歸添加。目錄結構也會被保持。

  • 使用 /x 可以用來來排除文件或目錄。

  • 如果使用了 /a ,則被添加的文件的屬性將會保持。

  • 如果覆蓋模式被設定為 try 但是文件不能覆蓋,那么 File 命令將會置一個錯誤標記,或者如果覆蓋模式被設定為 on 並且文件不能覆蓋並用戶選擇了忽略時,也會放置一個錯誤標記。

  • 如果使用了 /nonfatal 且當文件未找到時使用警告來代替錯誤。

File something.exe
File /a something.exe
File *.exe
File /r *.dat
File /r data
File /oname=$TEMP\temp.dat somefile.ext
File "/oname=$TEMP\name with spaces.dat" somefile.ext
File /nonfatal "一個可能不存在的文件"
File /r /x CVS myproject\*.*
File /r /x *.res /x *.obj /x *.pch source\*.*
Exec

語法:Exec 命令

執行一個指定的程序並且立即繼續安裝。注意指定的文件必須存在於目標系統而不是編譯的系統。 $OUTDIR 用於指定工作路徑。如果該命令不能被運行則會置一個錯誤標記。注意,如果該命令包含空格,你要用引號來把他們包括起來。例如: Exec '"$INSTDIR\command.exe" 參數' 。如果你不用引號括起來則在 Windows 9x下正常或丟失參數。

Exec '"$INSTDIR\someprogram.exe"'
Exec '"$INSTDIR\someprogram.exe" 某些參數'

注冊表、INI文件指令

WriteINIStr

語法:WriteINIStr INI文件 區段名 項 值

把“項” =“值”寫入“INI文件”的“區段名”區段。如果 INI文件不能寫入則放置一個錯誤的標記。

WriteINIStr $TEMP\something.ini section1 something 123
WriteRegStr

語法:WriteRegStr 根鍵 子鍵 項 值

把字符串寫入注冊表。

WriteRegStr HKLM "Software\My Company\My Software" "String Value" "dead beef"

WriteRegStr和WriteRegExpandStr不知道是啥個關系

拓展WriteRegExpandStr

語法也相同:WriteRegExpandStr 根鍵 子鍵 項 值

把字符串寫入注冊表。 root_key 必須為下面列表之一:

  • HKCRHKEY_CLASSES_ROOT
  • HKLMHKEY_LOCAL_MACHINE
  • HKCUHKEY_CURRENT_USER
  • HKUHKEY_USERS
  • HKCCHKEY_CURRENT_CONFIG
  • HKDDHKEY_DYN_DATA
  • HKPDHKEY_PERFORMANCE_DATA
  • SHCTXSHELL_CONTEXT

如果 根鍵SHCTXSHELL_CONTEXT, 當 SetShellVarContext 設置為 all 時,它將被替換成HKLM ;當 SetShellVarContext設置為 current 時,它將被替換成 HKCU

如果字符串不能寫入注冊表,則放置一個錯誤的標記。字符串的類型為 REG_SZ 對應 WriteRegStr ,或 REG_EXPAND_STR 對應 WriteRegExpandStr 。如果注冊表鍵不存在,則會自動創建。

WriteRegExpandStr HKLM "Software\My Company\My Software" "Expand String Value" "%WINDIR%\notepad.exe"
ReadRegStr

語法:ReadRegStr 用戶變量(輸出) 根鍵 子鍵 項

從注冊表讀取一個字符串值並輸出到用戶變量 $x 。有效的根鍵值在后面的 WriteRegStr 列出。

如果字符串不存在,會放置一個錯誤標記並把 $x 設為空字符串 ("") 。如果該值存在且為 REG_DWORD ,則會轉換為字符串類型並放置一個錯誤標記。

ReadRegStr $0 HKLM Software\NSIS ""
DetailPrint "NSIS is installed at: $0"

常規用途指令

CreateDirectory

語法:CreateDirectory 要創建的路徑

創建 (遞歸創建)指定的目錄。當目錄不能創建時會放置一個錯誤標記。

你也可以指定一個絕對路徑。

CreateDirectory $INSTDIR\some\directory
CreateShortCut

語法:CreateShortCut 快捷文件.lnk 目標文件 [參數 [圖標文件 [圖標索引號 [啟動選項 [鍵盤快捷鍵 [描述]]]]]]

創建一個指向 '目標文件' 的快捷方式 '快捷文件.lnk' ,可以帶 '參數' 參數。

用於快捷方式的圖標為 "圖標文件,圖標索引號" ;要使用默認圖標的話把 "圖標文件" 和 "圖標索引號" 設為空字符串。

"啟動選項" 可以是它們之一: SW_SHOWNORMAL, SW_SHOWMAXIMIZED, SW_SHOWMINIMIZED, 或一個空字符串。

"鍵盤快捷鍵" 應該為 'flag|c' 格式且 flag 可以聯合使用 (使用 |) : ALT, CONTROL, EXT, 或 SHIFT 。c 為要使用的字符 (a-z, A-Z, 0-9, F1-F24, 等等)。注意在這些字串里不能含有空格。一個典型的例子為 "ALT|CONTROL|F8" 。

$OUTDIR 被用來作為工作目錄。你可以在創建快捷方式之前使用 SetOutPath 來指定或更改。

"描述" 為快捷方式的描述,或在 XP 下作為注釋調用。 當快捷方式不能創建的時會放置一個錯誤標記(例如: 路徑 (鏈接路徑或目標路徑) 不存在或一些其它錯誤)。

CreateShortCut "$SMPROGRAMS\My Company\My Program.lnk" "$INSTDIR\My Program.exe" \
	"some command line parameters" "$INSTDIR\My Program.exe" 2 SW_SHOWNORMAL \
	ALT|CONTROL|SHIFT|F5 "a description"
WriteRegStr

語法:WriteRegStr 根鍵 子鍵 項 值

把字符串寫入注冊表。詳細信息請查看 WriteRegExpandStr。

WriteRegStr HKLM "Software\My Company\My Software" "String Value" "dead beef"

流程控制指令

Abort

語法:Abort [用戶信息]

取消安裝,停止執行腳本,並且在狀態顯示里顯示[用戶信息]。

注意:你可以用於回調函數來實現一些特殊功能。頁面回調也可以用 Abort 來實現特殊目的。

MessageBox

語法:MessageBox 消息框選項列表 消息框文本 [/SD返回] [返回值 對應的跳轉標記] [返回值2 對應的跳轉標記2]

顯示一個包含“消息框文本”的消息框。“消息框選項列表”必須為下面的一個或多個,多個使用 | 來隔開(例如 MB_YESNO|MB_ICONSTOP)。

MB_OK - 顯示 OK 按鈕
MB_OKCANCEL - 顯示 OK 和取消按鈕
MB_ABORTRETRYIGNORE -顯示退出、重試、忽略按鈕
MB_RETRYCANCEL -顯示重試和取消按鈕
MB_YESNO - 顯示是和否按鈕
MB_YESNOCANCEL -顯示是、否、取消按鈕
MB_ICONEXCLAMATION -顯示驚嘆號圖標
MB_ICONINFORMATION -顯示信息圖標
MB_ICONQUESTION -顯示問號圖標
MB_ICONSTOP - 顯示終止圖標
MB_TOPMOST - 使消息框在最前端顯示
MB_SETFOREGROUND -設置前景
MB_RIGHT - 右對齊文本
MB_RTLREADING - RTL閱讀次序
MB_DEFBUTTON1 -默認為按鈕 1
MB_DEFBUTTON2 -默認為按鈕 2
MB_DEFBUTTON3 -默認為按鈕 3
MB_DEFBUTTON4 -默認為按鈕 4

“返回值”可以為 0(或空,或保留關閉),或下列之一:
IDABORT - 退出按鈕
IDCANCEL - 取消按鈕
IDIGNORE - 忽略按鈕
IDNO - 否按鈕
IDOK - OK 按鈕
IDRETRY - 重試按鈕
IDYES - 是按鈕

如果消息框的返回值為對應的“返回值”,則安裝程序執行跳轉。
用 /SD 來指定一個上面列出的返回值當在安裝程序靜默安裝時作為返回值
;終於明白了。
;NSIS可以設置靜默安裝、卸載。也就是安裝、卸載過程完全不需要用戶參與,也沒有顯示頁面。此因此用戶無法對這些詢問框做出應答。此時使用“/SD”給詢問框一個默認進行的選項。

;MessageBox也是純順序執行。若設置了[返回值 相應跳轉標記]就跳向標記。否則繼續向下執行
;如下:例1
;首先MessageBox設置了了[返回值 相應跳轉標記],即[返回IDYES,就跳轉true標記][返回IDNO,就跳轉false標記]
;所以返回IDYES時,跳轉true輸出“是真的!”然后跳轉next繼續執行。
;返回IDNO時,跳轉false輸出"是假的"。
;然后會繼續執行后續指令next
;再次設計一個MessageBox,[返回IDNO 就跳轉false2標記] [當靜默安裝,默認返回IDYES],但是沒有設置IDYES的對應跳轉
;我理解這種情況,返回IDYES時,就應該默認繼續執行下一條指令,
;即輸出“是真的 (或靜默)!”,然后跳轉next2
;當返回IDNO,跳轉false2,輸出“是假的”,然后默認執行下一條指令,即next2:
MessageBox MB_OK "簡單消息框"
MessageBox MB_YESNO "真的嗎?" IDYES true IDNO false
true:
  DetailPrint "是真的!"
  Goto next
false:
  DetailPrint "是假的"
next:
    MessageBox MB_YESNO "真的嗎?(靜默安裝時默認為是)" /SD IDYES IDNO false2
      DetailPrint "是真的 (或靜默)!"
      Goto next2
    false2:
      DetailPrint "是假的"
    next2:

一開始糾結了很久這個標記的范圍。后來感覺好像沒范圍,就是順序執行。暫時就都這么理解。

文件指令

卸載程序指令

WriteUninstaller

語法:WriteUninstaller [路徑\]可執行文件名.exe

由指定的文件名(路徑為可選項)寫入卸載程序。僅在一個安裝區段或函數里有效,並且你的腳本里必須有一個卸載區段。也可以參考卸載配置。你可以調用一次或多次來寫入一個或多個卸載程序(副本)。

我理解就是生成執行卸載的那個執行文件。

混合指令

字符串操作指令

堆棧指令

Exch

語法:Exch [用戶變量 | 堆棧索引]

當不指定參數時,交換堆棧頂部的兩個單元。

當指定了一個參數並且是一個用戶變量時,交換堆棧頂部的單元和該變量的值。

當指定了一個參數並且是正整數時,Exch 將會交換堆棧頂部單元和根據參數從堆棧頂部偏移到指定單元的值。

如果堆棧里沒有足夠的單元來完成交換時,會產生一個致命的錯誤(來幫助你調試你的代碼)。

Push 1
Push 2
Exch
Pop $0 # 輸出 1

Push 1
Push 2
Push 3
Push 4
Push 5
Exch 2
Pop $0 # 輸出 3

StrCpy $0 1
Push 2
Exch $0 # = 2
Pop $1 # = 1
Pop

語法:Pop 用戶變量(輸出)

Push

整數支持

重新啟動指令

安裝記錄指令

區段管理

用戶界面指令

HideWindow

隱藏安裝程序。

Function un.onUninstSuccess
  HideWindow
FunctionEnd
SetAutoClose

語法:SetAutoClose true|false

取代默認的窗口自動關閉標記(由 AutoCloseWindow指定,且對於寫在程序為 false)。指定 true將使得安裝程序在安裝完成時立即關閉窗口,或者 false 來使它需要手動關閉。

多語言指令

編譯命令

基礎編譯命令

!include

語法:!include [/NONFATAL] 文件

這個命令可以將一個文件包含到腳本中,就像是腳本的一部分一樣。請注意,如果一個文件包含在了另一個目錄中,那么當前目錄仍是編譯腳本的地方(不是要包含的文件所在的目錄)。如果編譯器無法找到文件,那么它將會在每一個包含目錄中查找。

這怎么看都像機翻。一個文件包含在另一個目錄啥意思?沒有路徑!include默認去哪里找這個文件額,搞不明白。

如果使用了 /nonfatal 的話,當要包含的文件未找到的時候用警告來代替錯誤。

; MUI 1.67 compatible ------ 導入現代界面的相關文件
!include "MUI.nsh"

預定義

讀環境變量

條件編譯

編譯器有一個定義符號列表,定義符號可以通過 !define 定義或使用 /D 命令行切換。這些定義符號可以用於條件編譯 (通過 !ifdef 定義) 或用於符號替換(一種格式簡單的宏)。若要用它的值替換一個符號,請使用 ${符號} (如果 "符號" 沒有定義,那么不會產生轉換)。這個轉換為先到先得(first-come-first-served),這意味着如果你做了:

!define 符號1 ${符號2}

如果定義了 "${符號2}" ,那么當出現那一行時,它將會被替換。否則,當 ${符號1} 被引用時,任何替換都會發生。

沒懂

與定義、條件編譯相關的命令:

!define

語法:!define ([/date|/utcdate] 符號 [值]) | (/math 符號 值1 運算符 值2) | (/file 符號 filename.txt)

這個命令將會向全局定義列表中添加“符號”。這個效果與編譯器使用 /D 命令行切換效果相似(只有在 !define 命令之后,定義才有效)。

如果使用了 /date/utcdate,則定義的值會被格式化為 strtime格式。strtime 會把代表當前的時間日期轉換為實際的值。例如 %H會轉換為當前時間的 24 小時格式。

如果使用了 /math ,把(值1 運算符 值2)的運算結果作為 符號 的定義值。“運算符” 在這里可以是 +,-,*,&,|,^,/ 或 % 。請注意,值1 和 值2 都必須為整型值!

如果使用了 /file ,把指定的整個文本文件(包括空白和新行)讀取並填充到 符號 中。

!define USE_SOMETHING
!define VERSION 1.2
!define /date NOW "%H:%M:%S %d %b, %Y"
!define /math RESULT 3 + 10
!define /math REST 15 % ${RESULT}
!define /file BUNCHASTUFF somesourcefile.cpp

這和定義變量有啥區別?編譯過程中可用?

!macro !insertmacro

語法: !macro 宏名稱 [參數] [……] !insertmacro 宏名稱 [參數] [……]

插入一個由 !macro 創建的宏的內容。如果創建的宏帶有參數,那么你必須按宏的需求向它傳送足夠的參數。

調用函數的感覺。

; !macro 定義一個宏
!macro MyPrint text
	DetailPrint "${text}"
!macroend

; !insertmacro 調用這個宏
!insertmacro Print "show these contents"
!if !else !endif

語法:!if [!] 值 [運算符 值2] xxx !endif

語法:!else [if|ifdef|ifndef|ifmacrodef|ifmacrondef [...]]

當這個命令與 !endif 命令組成一對時,將會告訴編譯器是否編譯含在其兩者之間的代碼。

如果 為非零 或者 值2 的邏輯運算結果為真,那么所含的代碼將會被編譯。否則,那些代碼將會被跳過。

運算符 可以是 == or != (字符串比較), <=, < > 或 >= (實數比較), && or || (布爾運算)。 如果設置了 [!] ,則邏輯運算結果取非。

當有不同的定義或設置了不同的宏時,!else命令允許輕松插入不同的代碼。你可以創建類似 !ifdef/!else/!endif, !ifdef/!else if/!else/!endif 等代碼塊。

!if 1 < 2
  !echo "1 is smaller than 2!!"
!else if ! 3.1 > 1.99
  !error "this line should never appear"
!else
  !error "neither should this"
!endif


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM