How do I create a shortcut (.lnk) with a relative target? - windows

I have an executable on my disk-on-key in dir\program\prog.exe
I'd like to have a shortcut to the executable on the DoK's root directory, that is, prog.lnk would refer to dir\program\prog.exe.
However, it seems that prog.lnk can't have a relative target. This is a problem when the DoK will have different drive letters assigned to it, depending on which PC it's connected to.
Any suggestions, aside from the obvious one of putting prog.exe in the root dir?
(ultimately, I'd like to do this at install time using nsis)
Thanks,
Rony

If we assume cmd.exe would be on the same absolute path for all windows installations (probable, but not fool-proof) you can make out the .lnk file to start cmd like this
cmd.exe /c start /d. your command here
/d sets the directory to the directory of the .lnk file
There might be other useful options for the start command (e.g. /b)

While it is possible for shortcuts to contain a relative path to the target (.lnk files have a flag called SLDF_HAS_RELPATH) NSIS does not support creating anything other than "normal" shortcuts so you need to write the binary data directly (The .lnk format is pretty stable and has been documented by MS)
!macro FileWriteHexBytes h b
push ${h}
push ${b}
call FileWriteHexBytes
!macroend
Function FileWriteHexBytes
exch $9
exch
exch $0
push $1
push $2
loop:
StrCpy $2 $9 2
StrLen $1 $2
IntCmp $1 2 0 end
FileWriteByte $0 "0x$2"
StrCpy $9 $9 "" 2
goto loop
end:
pop $2
pop $1
pop $0
pop $9
FunctionEnd
Function CreateRelativeLnk
exch $9
exch
exch $0
push $1
FileOpen $0 "$0" w
StrCmp $0 "" clean
!insertmacro FileWriteHexBytes $0 "4C0000000114020000000000C000000000000046"
!insertmacro FileWriteHexBytes $0 48010400 ;flags
!insertmacro FileWriteHexBytes $0 00000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000
StrLen $1 $9 ;must be < 255!
FileWriteByte $0 $1
FileWriteByte $0 0
FileWrite $0 "$9" ;relative target path
!if 0
;The icon is problematic, does not seem like it works with relative paths (but you can use system icons...)
StrCpy $9 "explorer.exe"
StrLen $1 $9
FileWriteByte $0 $1
FileWriteByte $0 0
FileWrite $0 "$9"
!else
!insertmacro FileWriteHexBytes $0 05003e2e657865 ;fake default .exe icon
!endif
clean:
FileClose $0
pop $1
pop $0
pop $9
FunctionEnd
Call it like this:
push "$temp\testlink.lnk"
push "testdir\testapp.exe" ;full path to this is $temp\testdir\testapp.exe
call CreateRelativeLnk
While the generated .lnk seems to work, I'm not sure if I would use this in production code
A much better solution is to create a little NSIS app like Oleg suggests (NSIS applications can contain embedded data at the end of the .exe that it can read from itself at runtime etc..)

Install "Relative"
I'm using a bit of a hack. The approach is shown in this screenshot:
It starts explorer.exe and then passes a relative path like so:
%windir%\explorer.exe path\to\your\files\youFileName.example
I'm using a small tool called "Relative" for this. After you install it, you can right-click a file and then select Create relative shortcut.... It will then pop up a Save as... box. This is not quite as comfortable as simply dragging and dropping but it helps. (It also uses its own special icon for the links it creates. So you no longer have the original icon in the link. See above. You may or may not like this.)

You are right. You can not use shortcuts (.lnk) on a removable media, because like you wrote yourself the removable media can have different drive letters in different situations.
If you need to have something in a root directory of the drive one use a CMD, VBS or JS instead of a shortcut. In some situation one use also a standard EXE which use a config file like INI file to start another program from the other subdirectory. It is a typical practice for CD/DVD to implement starting of a setup program after inserting of the disk. Probably this way will be OK in for your requirements also?

I believe that the NT 4 Resource Kit command SHORTCUT.exe would create relative linked shortcuts. I wish that Microsoft would create a new supported utility or Powershell Cmdlet to facilitate the creation of relative .lnk files or make NTFS links work more like Linux symbolic links. Until then, I use .cmd/.bat and .ps1 files for this purpose.
Program.cmd
#"%~dp0Relative\Path\Program.exe" %*
# = Suppresses the command echo.
%~dp0 = expands to the script's directory.
Relative\Path = could include .. to backup a directory.
%* = passes any parameters received by the script on to Program.exe.
Program.ps1
Unfortunately, though .cmd/.bat files will run from any context (the run dialog, a CMD prompt, in Powershell, double clicking in File Explorer, etc), they cannot call things stored on a UNC path. For exposing things in a UNC path, in Powershell (I do this for tools like Git and Mercurial), I will create a Powershell version of the above script.
&"$PSScriptRoot\Relative\Path\Program.exe" #args
& = puts Powershell into command mode, so that the string in quotes gets ran.
"" = contains a string, expanding any variables.
$PSScriptRoot = expands to the script's directory.
Relative\Path = could include .. to backup a directory.
#args = passes any parameters received by the script on to Program.exe.

Related

How to get network path of a directory using command line

Say I have a directory called Temp in windows.
When I right click on it and open properties, and go to Sharing tab, I see this:
The field Network Path is Not Shared.
Now I click Share and select to share with and click ok.
The field Network Path now has value:
Is there a way to get the value of Network Path from command line.
Ideally I am looking for a command which takes directory path as input e.g. C:\Temp and output \\S5XXXXXXN\Temp if the directory is shared or Not Shared if the directory is not shared.
Background: I have a NSIS script to create an installer. The end of installation process is supposed to spit out a list of properties. One of the properties required for output is the Network Path of a directory. Inside NSIS what I have available is C:\Temp which I want to convert to \\S5XXXXXXN\Temp (if available) before spitting it out.
I tried to use this https://nsis.sourceforge.io/Get_Universal_Name but its not working.
DetailPrint "My directory is:"
# value of MYDIR is C:\Temp
DetailPrint $MYDIR
Push $MYDIR
Call get_universal_name
Pop $MYDIRNET
DetailPrint "My dir after call is: "
DetailPrint $MYDIRNET
# above also prints C:\Temp even though its shared and has value \\S5XXXXXXN\Temp
Update:
The answer below by #Anders works like a charm.
The list returned has an entry with blank $3 though. Just to be aware of and to add a necessary check if required.
get_universal_name calls WNetGetUniversalName and this function only supports mapped drive letters (net.exe use style). WNetGetConnection seems to have the same limitation. Even if this limitation did not exist it would still not work because a process elevated with UAC does not share the net connections with non-elevated processes.
Going the other way does seem to work, getting a list of shares and their local paths:
!include LogicLib.nsh
Section "Check shares"
System::Call 'NETAPI32::NetShareEnum(p0,i2,*p.r1,i-1,*i0r5,*i0,p0)i.r0'
${If} $0 = 0
Push $1
${DoWhile} $5 <> 0
System::Call '*$1(p.r2,i,p,i,i,i,p.r3,p,&l.r4)'
IntPtrOp $1 $1 + $4
System::Call '*$2(&w${NSIS_MAX_STRLEN}.r2)'
System::Call '*$3(&w${NSIS_MAX_STRLEN}.r3)'
System::Call 'NETAPI32::NetServerGetInfo(p0,i100,*p.r4)' ; Get computer name
System::Call '*$4(i,w.s)'
System::Call 'NETAPI32::NetApiBufferFree(p$4)'
Pop $4
DetailPrint "$3=\\$4\$2"
; ${If} $3 == "$MYDIR" ...TODO... ${EndIf}
IntOp $5 $5 - 1
${Loop}
System::Call 'NETAPI32::NetApiBufferFree(ps)'
${EndIf}
SectionEnd

NSIS - Install a file to a pre-existing, uncertain subfolder?

I'm trying to install a file to a pre-existing folder structured like this:
$APPDATA/somefolder/(uncertainFolder)
The "uncertainFolder" would be either "1.0" or "2.0".
The same file will be installed into the "uncertainFolder" despite the folder name difference. How can I achieve this?
Thank you in advance.
Files installed with the File instruction are extracted into the directory set by SetOutPath. Changing this path at run-time is not a problem once you know which folder you want.
If the possible folder names are known at compile-time you can use if/or if/else:
!include LogicLib.nsh
${If} ${FileExists} "$InstDir\SomeFolder\2.0\*.*"
SetOutPath "$InstDir\SomeFolder\2.0"
${Else}
SetOutPath "$InstDir\SomeFolder\1.0"
${EndIf}
You can also enumerate files and folders at run-time:
FindFirst $0 $1 "$InstDir\SomeFolder\*.*"
loop:
StrCmp $1 "" end ; No more files?
StrCmp $1 "." next ; DOS special name
StrCmp $1 ".." next ; DOS special name
IfFileExists "$InstDir\SomeFolder\$1\*.*" 0 next ; Skip files
DetailPrint "$1 is a folder in $InstDir\SomeFolder"
SetOutPath "$InstDir\SomeFolder\$1"
Goto end ; This stops the search at the first folder it finds
next:
FindNext $0 $1
goto loop
end:
FindClose $0
The Locate macro in FileFunc.nsh is built on top of FindFirst/FindNext and can also be used if you prefer its syntax...

Adding extra command to cpack - NSIS packager/installer

I want to package a project of mine for windows with CPack and NSIS using an already existing
GeneratorConfig.cmake file where I want to add an extra command that will copy an .ini file called myProject.ini into %APPDATA%/myProject/myProject.ini .
This is GeneratorConfig.cmake
SET(INSTALL_AN_ALREADY_EXISTING_DIR ".")
##########################################################################
## Begin NSIS Specific options
##------------------------------------------------------------------------
if(CPACK_GENERATOR MATCHES NSIS)
# Additional NSIS commands to uninstall start menu shortcuts
SET(CPACK_NSIS_DELETE_ICONS_EXTRA
"Delete \"$SMPROGRAMS\\$MUI_TEMP\\${PROJECT_NAME}.lnk\"
StrCmp \"$INSTALL_DESKTOP\" \"1\" 0 +2
Delete \"$DESKTOP\\${PROJECT_NAME}.lnk\" ")
# The display name string that appears in the Windows Add/Remove Program control panel
SET(CPACK_NSIS_DISPLAY_NAME "${PROJECT_NAME} ${PROJECT_VER_MAJOR}.${PROJECT_VER_MINOR}")
SET(CPACK_NSIS_DISPLAY_NAME_SET "TRUE")
# Extra NSIS commands that will be added to the end of the install Section, after your
# install tree is available on the target system.
SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS
"CreateShortCut \"$SMPROGRAMS\\$STARTMENU_FOLDER\\${PROJECT_NAME}.lnk\" \"$INSTDIR\\.\\bin\\${PROJECT_EXE} \" -previousworkingdir \"$INSTDIR\\.\\bin\\app_icon.ico\"
StrCmp \"$INSTALL_DESKTOP\" \"1\" 0 +2
CreateShortCut \"$DESKTOP\\${PROJECT_NAME}.lnk\" \"$INSTDIR\\.\\bin\\${PROJECT_EXE} \" -previousworkingdir \"$INSTDIR\\.\\bin\\app_icon.ico\"
")
# Extra commands to fix permissions of bin/licenses folder
SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS
"${CPACK_NSIS_EXTRA_INSTALL_COMMANDS}
ExecWait 'icacls \\\"$INSTDIR\\\\bin\\\\licenses\\\" /grant:r Users:\\\(OI\\\)\\\(CI\\\)\\\(F\\\)'
")
# A path to the executable that contains the installer icon.
SET(CPACK_NSIS_INSTALLED_ICON_NAME "${PROJECT__DIR}\\bin\\${PROJECT_EXE}")
# The default installation directory presented to the end user by the NSIS installer
SET(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
# Title displayed at the top of the installer
SET(CPACK_NSIS_PACKAGE_NAME "${PROJECT_NAME} ${PROJECT_VER_MAJOR}.${PROJECT_VER_MINOR}")
SET(CPACK_NSIS_PAGE_COMPONENTS " ")
SET(CPACK_NSIS_MUI_FINISHPAGE_RUN ${PROJECT_EXE})
endif(CPACK_GENERATOR MATCHES NSIS)
##------------------------------------------------------------------------
## End NSIS Specific options
##########################################################################
I tried to do this with the code below but this builds the package but doesn't
copy myProject.ini anywhere.
SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS
"CreateDirectory \"$ENV{APPDATA}\\myProject\",
SetOutPath \"$ENV{APPDATA}\\myProject\",
File \"myProject.ini\"
")
Any help or suggestions will be appreciated.
From what i can tell you are using not enough '\' symbols.
To clarify: that string is evaluated twice, once by CMake and once by CPack, and each time substitutions such as \->\ and \" -> " occur. Please check generated nsis project for what the actual generated commands are and whether all '"' are correctly set.
To summarize: try using this:
SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS
"CreateDirectory \\\"$ENV{APPDATA}\\\\myProject\\\",
SetOutPath \\\"$ENV{APPDATA}\\\\myProject\\\",
File \\\"myProject.ini\\\"
")

NSIS - EnvVarUpdate overwrites system path when path is too long, is there a workaround?

Here is my simple code:
!include "EnvVarUpdate.nsh"
Outfile "text.exe"
Section
${EnvVarUpdate} $0 "PATH" "A" "HKLM" "C:\Program Files\something"
SectionEnd
I understand that the "A" argument means this should APPEND the last argument to system path. However, testing this revealed that it overwrote my Path variable. Further tests reveal this is because Path was too long (>1024 chars, per the tutorial).
Is there a "safe" way to append to Path then? I am looking for a function that will append if Path is short enough, otherwise do nothing and report an error, something of that sort. I'm wondering if a standard method of doing this already exists. Thanks!
We have encountered some problems with path modifications from NSIS installers due to the fact that the default string management is limited to 1024 bytes and that string manipulations involved in path modification is truncating strings to 1024 and that is sometimes braking workstation environment (especially in development hosts where many tools are installed). BTW, There are many nsis built setups in the wild that are suffering from this problem.
We are using some different code derived from the AddToPath function from Path manipulation but the problem stays similar.
The best workaround that we are using until now is by using the special build of NSIS that provides the large string support (of 8kB instead of 1 kB). That build is available in the special builds page of the NSIS wiki.
Can you try this?
Section
ReadEnvStr $0 PATH
StrCpy $0 "$0;C:\Program Files\something"
StrLen $1 $0
${if} $1 < 1024
${EnvVarUpdate} $0 "PATH" "A" "HKLM" "C:\Program Files\something"
${else}
messagebox mb_ok "error writing environment variable"
${endIf}
SectionEnd

How to load a file's contents into a variable/define in NSIS?

I have a file 'releaseVersionNumber.txt' which I read during my build process; currently its read for my Mac build but I want to read it in my Windows NSIS build to reduce the number of edit locations (duplication being evil)...
So I'm trying to replace:
!define VERSION 1.2.3
with something like
FileOpen $4 "..\releaseVersionNumber.txt" r
FileRead $4 $1
FileClose $4
!define VERSION ${1}
But I get an error command FileOpen not valid outside Section or Function. Wrapping it in a function I produces command call not valid outside Section or Function so I can't seem to do this in the installer setup, only at runtime.
Is there a way to achieve what I'm after?!
All commands begining with ! are compile time commands, so they are processed at compile time, much before your program runs.
You can try declaring VERSION as a Variable instead of a define:
Var VERSION
FileOpen $4 "..\releaseVersionNumber.txt" r
FileRead $4 $VERSION
FileClose $4
If you need VERSION to be a define, then you can try the /file parameter in !define.
!define /file VERSION "..\releaseVersionNumber.txt"
I like to have a version.nsh file with just the define:
!define VERSION "2013-03-25:16:23:50"
And then, I include it:
!include /NONFATAL version.nsh
# Default value in case no version.nsh is present
!ifndef VERSION
!define /date VERSION "%Y-%m-%d %H:%M:%S"
!endif

Resources