{"record":{"id":"dfc862a08a85fb34","repo":"jdx/mise","slug":"cannot-copy-a-file-or-symlink-to-the-image-root","errorCode":null,"errorMessage":"cannot copy a file or symlink to the image root","messagePattern":"cannot copy a file or symlink to the image root","errorType":"exception","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"src/oci/layer.rs","lineNumber":123,"sourceCode":"}\n\n/// Build a reproducible layer from one host file, symlink, or directory.\n/// Directory contents are placed under `image_path`; a file or symlink is\n/// placed at `image_path` itself.\npub fn build_layer_from_path(\n    host_path: &Path,\n    image_path: &str,\n    owner: LayerOwner,\n) -> Result<LayerBlob> {\n    let metadata = std::fs::symlink_metadata(host_path)\n        .wrap_err_with(|| format!(\"reading metadata for {}\", host_path.display()))?;\n    let target = image_path.trim_matches('/');\n\n    if metadata.is_dir() {\n        return build_layer_from_dir(host_path, target, owner);\n    }\n    if target.is_empty() {\n        eyre::bail!(\"cannot copy a file or symlink to the image root\");\n    }\n    if image_path.ends_with('/') {\n        eyre::bail!(\n            \"copy image path {image_path:?} ends with `/` but {} is not a directory; \\\n             specify the exact destination file name\",\n            host_path.display()\n        );\n    }\n\n    let kind = if metadata.file_type().is_symlink() {\n        EntryKind::Symlink(\n            std::fs::read_link(host_path)\n                .wrap_err_with(|| format!(\"reading symlink {}\", host_path.display()))?,\n        )\n    } else if metadata.is_file() {\n        EntryKind::File\n    } else {\n        eyre::bail!(\"unsupported host path type: {}\", host_path.display());","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/oci/layer.rs#L105-L141","documentation":"build_layer_from_path trims leading/trailing slashes from image_path; for a file or symlink source the trimmed target must be non-empty because the entry is placed at that exact path. A destination of \"/\" or \"//\" trims to \"\", so mise refuses rather than writing a file at the image root.","triggerScenarios":"A copy-style entry whose destination is \"/\" (or any all-slash string) while the source is a regular file or symlink.","commonSituations":"Thinking \"/\" means 'copy into the root directory' (which would need a directory source plus a name); template-generated destinations that collapse to empty and get a default '/' appended.","solutions":["Give the file a concrete destination, e.g. \"/usr/local/bin/mytool\" or \"/etc/motd\".","If you meant to copy a whole tree to the root, make the source a directory (directories are allowed to target '/')."],"exampleFix":"# before\nfiles = [ \"./bin/mytool:/\" ]\n\n# after\nfiles = [ \"./bin/mytool:/usr/local/bin/mytool\" ]","handlingStrategy":"validation","validationCode":"import os, sys\nfor src, dst in copy_entries.items():\n    if not os.path.isdir(src) and dst.strip('/') == '':\n        sys.exit(f\"file source {src} needs a full destination path, got {dst!r}\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always spell the complete destination file path for file/symlink sources.","Reserve '/' destinations for directory sources only.","Lint copy entries in CI for empty-or-slash destinations on non-directory sources."],"tags":["oci","layer","destination-path","validation"],"backgroundTag":"invalid-destination-path","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}