デスクトップにショートカットを作成する

Windows の NSIS / MSI インストーラーは設定なしでデスクトップにショートカットを作る。その条件と、ポータブル版向けに Rust から PowerShell で .lnk を作る方法、存在しない設定キーの落とし穴も示す。

システム情報 対象: Tauri 2.x 更新日: 読了目安: 約9分 sys-023
目次
  1. 前提条件
  2. 1. インストーラーに作らせる (tauri.conf.json)
  3. 2. バックエンドから実装する (Rust)
  4. 動作確認
  5. よくあるエラーと対処法
  6. OS ごとの違いと注意点
  7. 関連レシピ

デスクトップのショートカットは、アプリのコードで作るよりインストーラーに任せるのが確実です。Tauri が Windows 向けに作る NSIS と MSI のインストーラーは、設定を書かなくてもデスクトップとスタートメニューにショートカットを作り、アンインストール時に消します。インストーラーを通らないポータブル版(exe を zip で配る形)や、ユーザーが消したショートカットを設定画面から作り直したい場合は、Rust から OS 標準の仕組みを呼んで作ります。どちらも追加のクレートは要りません。

前提条件

インストーラーで作るだけなら、プラグインも権限も不要です。2 章の例では作る前に確認ダイアログを出すので、Dialog プラグインを追加します。確認に使う ask() と結果表示の message() は dialog:default で使えます。

npm run tauri add dialog
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "windows": ["main"],
  "permissions": ["core:default", "dialog:default"]
}

1. インストーラーに作らせる (tauri.conf.json)

形式ごとの動きは次のとおりです。ショートカットの名前はどれも productName になります。

形式デスクトップスタートメニューアンインストール時
NSIS(-setup.exe)完了画面のチェックボックス(既定でオン)常に作るどちらも消す
MSI常に作る(選ぶ画面は無い)常に作るどちらも消す
deb / rpm作らない(アプリ一覧に登録)―一覧から消える
AppImage / .app / DMG作らない――

NSIS の languages に Japanese を入れておくと、日本語の Windows では完了画面のチェックボックスが「デスクトップショートカットを作成する」と表示されます。スタートメニューの中でフォルダーにまとめたいときは startMenuFolder を指定します。

{
  "productName": "MyNotes",
  "bundle": {
    "active": true,
    "targets": ["nsis", "msi"],
    "windows": {
      "nsis": {
        "installMode": "currentUser",
        "languages": ["Japanese", "English"],
        "startMenuFolder": "My Company"
      }
    }
  }
}

ネットで見かける createDesktopShortcut や createStartMenuShortcut は、Tauri v2 の nsis にはありません。書くと設定の検証でエラーになります。チェックボックスを既定でオフにする、項目自体を消すといった変更は template に独自の .nsi を指定するしかなく、Tauri の更新に合わせて保守する手間がかかります。

サイレント(/S)とパッシブ(/P)のインストールでは完了画面が出ないため、デスクトップのショートカットも自動で作られます。社内への一括配布などで作らせたくないときは /NS を付けると、スタートメニューとデスクトップのどちらも作られません。

.\MyNotes_1.0.0_x64-setup.exe /P /NS

2. バックエンドから実装する (Rust)

実行中に作るのは、ポータブル版や「ショートカットを作り直す」ボタンを用意する場合です。Windows のショートカット(.lnk)はテキストファイルではないので、OS に付属する PowerShell の標準機能で作ります。パスは文字列に埋め込まず環境変数で渡すと、空白や ' を含むパスでも壊れません。置き場所は desktop_dir() で取得します。ユーザーがデスクトップの場所を移していることがあるので、C:\Users\<名前>\Desktop と決め打ちしません。

use std::path::Path;
use tauri::Manager;

#[derive(serde::Serialize)]
struct ShortcutResult {
    path: String,
    created: bool, // false なら既にあったので何もしていない
}

/// デスクトップに productName のショートカットを作る(既にあれば触らない)
#[tauri::command]
fn create_desktop_shortcut(app: tauri::AppHandle) -> Result<ShortcutResult, String> {
    let name = app.package_info().name.clone(); // productName
    let desktop = app.path().desktop_dir().map_err(|e| e.to_string())?;
    let exe = std::env::current_exe().map_err(|e| e.to_string())?;
    let ext = if cfg!(windows) { "lnk" } else { "desktop" };
    let path = desktop.join(format!("{name}.{ext}"));
    if path.exists() {
        return Ok(ShortcutResult { path: path.display().to_string(), created: false });
    }
    write_shortcut(&path, &exe, &name)?;
    Ok(ShortcutResult { path: path.display().to_string(), created: true })
}

#[cfg(windows)]
fn write_shortcut(lnk: &Path, exe: &Path, _name: &str) -> Result<(), String> {
    use std::os::windows::process::CommandExt;
    const CREATE_NO_WINDOW: u32 = 0x0800_0000; // PowerShell の黒い画面を出さない
    let script = "$s = (New-Object -ComObject WScript.Shell).CreateShortcut($env:LNK_PATH); \
                  $s.TargetPath = $env:LNK_TARGET; \
                  $s.WorkingDirectory = $env:LNK_DIR; \
                  $s.Save()";
    let status = std::process::Command::new("powershell")
        .args(["-NoProfile", "-NonInteractive", "-Command", script])
        .env("LNK_PATH", lnk)
        .env("LNK_TARGET", exe)
        .env("LNK_DIR", exe.parent().unwrap_or(exe))
        .creation_flags(CREATE_NO_WINDOW)
        .status()
        .map_err(|e| format!("PowerShell を起動できません: {e}"))?;
    if status.success() && lnk.exists() {
        Ok(())
    } else {
        Err(format!("ショートカットを作れませんでした ({status})"))
    }
}

#[cfg(target_os = "linux")]
fn write_shortcut(file: &Path, exe: &Path, name: &str) -> Result<(), String> {
    use std::os::unix::fs::PermissionsExt;
    // AppImage で動いているときは AppImage ファイル自体を起動させる
    let target = std::env::var_os("APPIMAGE")
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|| exe.to_path_buf());
    let entry = format!(
        "[Desktop Entry]\nType=Application\nName={name}\nExec=\"{}\"\nTerminal=false\n",
        target.display()
    );
    std::fs::write(file, entry).map_err(|e| e.to_string())?;
    std::fs::set_permissions(file, std::fs::Permissions::from_mode(0o755)).map_err(|e| e.to_string())
}

#[cfg(not(any(windows, target_os = "linux")))]
fn write_shortcut(_: &Path, _: &Path, _: &str) -> Result<(), String> {
    Err("この OS ではデスクトップにショートカットを作りません".into())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .invoke_handler(tauri::generate_handler![create_desktop_shortcut])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

ショートカットの行き先は current_exe()、つまり今動いている実行ファイルです。ユーザーが消したショートカットを起動のたびに作り直すと嫌がられるので、設定画面のボタンから、確認を取ってから呼びます(確認ダイアログ)。

import { invoke } from '@tauri-apps/api/core';
import { ask, message } from '@tauri-apps/plugin-dialog';

type ShortcutResult = { path: string; created: boolean };

// 設定画面の「デスクトップにショートカットを作成」ボタンから呼ぶ
export async function offerDesktopShortcut() {
  const yes = await ask('デスクトップにショートカットを作成しますか?', { title: 'ショートカット' });
  if (!yes) return;
  try {
    const r = await invoke<ShortcutResult>('create_desktop_shortcut');
    await message(r.created ? `作成しました: ${r.path}` : `既にあります: ${r.path}`);
  } catch (e) {
    await message(`作成できませんでした: ${e}`, { kind: 'error' });
  }
}

動作確認

インストーラーは npm run tauri build -- --bundles nsis で作り、src-tauri/target/release/bundle/nsis/ の -setup.exe を実行します。完了画面のチェックを付けたまま閉じるとデスクトップに「MyNotes」ができ、「アプリと機能」からアンインストールすると消えます。

実行中に作る方は、npm run tauri dev で起動して offerDesktopShortcut() を呼び、「はい」を選ぶと次のように表示されます。もう一度呼ぶと「既にあります」になります。

作成しました: C:\Users\taro\Desktop\MyNotes.lnk

tauri dev 中に作ったショートカットは開発用の実行ファイル(src-tauri/target/debug の中)を指すので、確かめたら削除します。

よくあるエラーと対処法

  • tauri build などで「is not valid under any of the schemas listed in the 'anyOf' keyword」で終わるエラー: 場所が bundle > windows > nsis と示されていれば、nsis に無いキー(createDesktopShortcut など)を書いています。消せば、ショートカットは既定の動きで作られます。
  • サイレントインストールでショートカットができない: /NS が付いていないか確かめます。
  • 「PowerShell を起動できません」「ショートカットを作れませんでした」: 会社の PC などで PowerShell の実行が制限されていると失敗します。インストーラーでの作成に切り替えるか、手で作る手順を案内します。
  • ショートカットを開くと「項目が見つからない」趣旨の表示になる: ポータブル版の exe を移動したか、開発中に作ったものです。削除して作り直します。
  • 作ったのにデスクトップに見当たらない: パスを自分で組み立てていないか確かめます。desktop_dir()(JS は desktopDir())の場所に作ります。

OS ごとの違いと注意点

  • Windows: NSIS はユーザーがデスクトップのショートカットを断れますが、MSI では選べません。ユーザーに選ばせたいなら NSIS を使います(NSIS / MSI)。
  • macOS: デスクトップにショートカットを置く習慣がなく、Tauri の .app / DMG も作りません。Dock への追加はユーザーに任せます。
  • Linux: deb / rpm はアプリ一覧に登録するための .desktop ファイルを入れます。中身は desktopTemplate で変えられます(Deb パッケージ)。AppImage はファイルを置くだけなので何も登録しません。デスクトップにアイコンを表示するか、置いた .desktop をそのまま起動できるかはデスクトップ環境の設定によります。
  • desktopDir() の場所: Linux は XDG_DESKTOP_DIR、macOS は $HOME/Desktop、Windows はユーザーのデスクトップフォルダーです。
  • ログイン時の起動も登録したい場合は OS 起動時にアプリを自動起動させる を参照してください。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する