diff --git a/Cargo.lock b/Cargo.lock
index 0369442f11c..650553e0d83 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3702,6 +3702,7 @@ dependencies = [
 name = "rustc_interface"
 version = "0.0.0"
 dependencies = [
+ "atty",
  "libloading",
  "rustc-rayon",
  "rustc-rayon-core",
@@ -4166,6 +4167,7 @@ dependencies = [
 name = "rustc_session"
 version = "0.0.0"
 dependencies = [
+ "atty",
  "getopts",
  "libc",
  "rustc_ast",
diff --git a/compiler/rustc_codegen_ssa/messages.ftl b/compiler/rustc_codegen_ssa/messages.ftl
index 9aa2b2e2b2e..5ecb63986fe 100644
--- a/compiler/rustc_codegen_ssa/messages.ftl
+++ b/compiler/rustc_codegen_ssa/messages.ftl
@@ -9,6 +9,8 @@ codegen_ssa_archive_build_failure =
 
 codegen_ssa_atomic_compare_exchange = Atomic compare-exchange intrinsic missing failure memory ordering
 
+codegen_ssa_binary_output_to_tty = option `-o` or `--emit` is used to write binary output type `{$shorthand}` to stdout, but stdout is a tty
+
 codegen_ssa_check_installed_visual_studio = please ensure that Visual Studio 2017 or later, or Build Tools for Visual Studio were installed with the Visual C++ option.
 
 codegen_ssa_copy_path = could not copy {$from} to {$to}: {$error}
diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs
index 8a00c42a0e8..311e56cc7d1 100644
--- a/compiler/rustc_codegen_ssa/src/back/link.rs
+++ b/compiler/rustc_codegen_ssa/src/back/link.rs
@@ -8,7 +8,7 @@ use rustc_errors::{ErrorGuaranteed, Handler};
 use rustc_fs_util::fix_windows_verbatim_for_gcc;
 use rustc_hir::def_id::{CrateNum, LOCAL_CRATE};
 use rustc_metadata::find_native_static_library;
-use rustc_metadata::fs::{emit_wrapper_file, METADATA_FILENAME};
+use rustc_metadata::fs::{copy_to_stdout, emit_wrapper_file, METADATA_FILENAME};
 use rustc_middle::middle::debugger_visualizer::DebuggerVisualizerFile;
 use rustc_middle::middle::dependency_format::Linkage;
 use rustc_middle::middle::exported_symbols::SymbolExportKind;
@@ -68,6 +68,7 @@ pub fn link_binary<'a>(
 ) -> Result<(), ErrorGuaranteed> {
     let _timer = sess.timer("link_binary");
     let output_metadata = sess.opts.output_types.contains_key(&OutputType::Metadata);
+    let mut tempfiles_for_stdout_output: Vec<PathBuf> = Vec::new();
     for &crate_type in sess.crate_types().iter() {
         // Ignore executable crates if we have -Z no-codegen, as they will error.
         if (sess.opts.unstable_opts.no_codegen || !sess.opts.output_types.should_codegen())
@@ -97,12 +98,15 @@ pub fn link_binary<'a>(
                 .tempdir()
                 .unwrap_or_else(|error| sess.emit_fatal(errors::CreateTempDir { error }));
             let path = MaybeTempDir::new(tmpdir, sess.opts.cg.save_temps);
-            let out_filename = out_filename(
+            let output = out_filename(
                 sess,
                 crate_type,
                 outputs,
                 codegen_results.crate_info.local_crate_name,
             );
+            let crate_name = format!("{}", codegen_results.crate_info.local_crate_name);
+            let out_filename =
+                output.file_for_writing(outputs, OutputType::Exe, Some(crate_name.as_str()));
             match crate_type {
                 CrateType::Rlib => {
                     let _timer = sess.timer("link_rlib");
@@ -152,6 +156,17 @@ pub fn link_binary<'a>(
                     );
                 }
             }
+
+            if output.is_stdout() {
+                if output.is_tty() {
+                    sess.emit_err(errors::BinaryOutputToTty {
+                        shorthand: OutputType::Exe.shorthand(),
+                    });
+                } else if let Err(e) = copy_to_stdout(&out_filename) {
+                    sess.emit_err(errors::CopyPath::new(&out_filename, output.as_path(), e));
+                }
+                tempfiles_for_stdout_output.push(out_filename);
+            }
         }
     }
 
@@ -189,6 +204,11 @@ pub fn link_binary<'a>(
             remove_temps_from_module(allocator_module);
         }
 
+        // Remove the temporary files if output goes to stdout
+        for temp in tempfiles_for_stdout_output {
+            ensure_removed(sess.diagnostic(), &temp);
+        }
+
         // If no requested outputs require linking, then the object temporaries should
         // be kept.
         if !sess.opts.output_types.should_link() {
diff --git a/compiler/rustc_codegen_ssa/src/back/write.rs b/compiler/rustc_codegen_ssa/src/back/write.rs
index c323372bda4..a1e8725e08e 100644
--- a/compiler/rustc_codegen_ssa/src/back/write.rs
+++ b/compiler/rustc_codegen_ssa/src/back/write.rs
@@ -23,12 +23,13 @@ use rustc_hir::def_id::{CrateNum, LOCAL_CRATE};
 use rustc_incremental::{
     copy_cgu_workproduct_to_incr_comp_cache_dir, in_incr_comp_dir, in_incr_comp_dir_sess,
 };
+use rustc_metadata::fs::copy_to_stdout;
 use rustc_metadata::EncodedMetadata;
 use rustc_middle::dep_graph::{WorkProduct, WorkProductId};
 use rustc_middle::middle::exported_symbols::SymbolExportInfo;
 use rustc_middle::ty::TyCtxt;
 use rustc_session::cgu_reuse_tracker::CguReuseTracker;
-use rustc_session::config::{self, CrateType, Lto, OutputFilenames, OutputType};
+use rustc_session::config::{self, CrateType, Lto, OutFileName, OutputFilenames, OutputType};
 use rustc_session::config::{Passes, SwitchWithOptPath};
 use rustc_session::Session;
 use rustc_span::source_map::SourceMap;
@@ -535,9 +536,16 @@ fn produce_final_output_artifacts(
     let mut user_wants_objects = false;
 
     // Produce final compile outputs.
-    let copy_gracefully = |from: &Path, to: &Path| {
-        if let Err(e) = fs::copy(from, to) {
-            sess.emit_err(errors::CopyPath::new(from, to, e));
+    let copy_gracefully = |from: &Path, to: &OutFileName| match to {
+        OutFileName::Stdout => {
+            if let Err(e) = copy_to_stdout(from) {
+                sess.emit_err(errors::CopyPath::new(from, to.as_path(), e));
+            }
+        }
+        OutFileName::Real(path) => {
+            if let Err(e) = fs::copy(from, path) {
+                sess.emit_err(errors::CopyPath::new(from, path, e));
+            }
         }
     };
 
@@ -547,7 +555,12 @@ fn produce_final_output_artifacts(
             //    to copy `foo.0.x` to `foo.x`.
             let module_name = Some(&compiled_modules.modules[0].name[..]);
             let path = crate_output.temp_path(output_type, module_name);
-            copy_gracefully(&path, &crate_output.path(output_type));
+            let output = crate_output.path(output_type);
+            if !output_type.is_text_output() && output.is_tty() {
+                sess.emit_err(errors::BinaryOutputToTty { shorthand: output_type.shorthand() });
+            } else {
+                copy_gracefully(&path, &output);
+            }
             if !sess.opts.cg.save_temps && !keep_numbered {
                 // The user just wants `foo.x`, not `foo.#module-name#.x`.
                 ensure_removed(sess.diagnostic(), &path);
diff --git a/compiler/rustc_codegen_ssa/src/errors.rs b/compiler/rustc_codegen_ssa/src/errors.rs
index cf4893b8226..3fed9ea0b41 100644
--- a/compiler/rustc_codegen_ssa/src/errors.rs
+++ b/compiler/rustc_codegen_ssa/src/errors.rs
@@ -82,6 +82,12 @@ impl IntoDiagnosticArg for DebugArgPath<'_> {
     }
 }
 
+#[derive(Diagnostic)]
+#[diag(codegen_ssa_binary_output_to_tty)]
+pub struct BinaryOutputToTty {
+    pub shorthand: &'static str,
+}
+
 #[derive(Diagnostic)]
 #[diag(codegen_ssa_ignoring_emit_path)]
 pub struct IgnoringEmitPath {
diff --git a/compiler/rustc_driver_impl/src/lib.rs b/compiler/rustc_driver_impl/src/lib.rs
index 40aa69e5a41..416603a0e1c 100644
--- a/compiler/rustc_driver_impl/src/lib.rs
+++ b/compiler/rustc_driver_impl/src/lib.rs
@@ -34,7 +34,9 @@ use rustc_interface::{interface, Queries};
 use rustc_lint::LintStore;
 use rustc_metadata::locator;
 use rustc_session::config::{nightly_options, CG_OPTIONS, Z_OPTIONS};
-use rustc_session::config::{ErrorOutputType, Input, OutputType, PrintRequest, TrimmedDefPaths};
+use rustc_session::config::{
+    ErrorOutputType, Input, OutFileName, OutputType, PrintRequest, TrimmedDefPaths,
+};
 use rustc_session::cstore::MetadataLoader;
 use rustc_session::getopts::{self, Matches};
 use rustc_session::lint::{Lint, LintId};
@@ -437,9 +439,12 @@ fn run_compiler(
 }
 
 // Extract output directory and file from matches.
-fn make_output(matches: &getopts::Matches) -> (Option<PathBuf>, Option<PathBuf>) {
+fn make_output(matches: &getopts::Matches) -> (Option<PathBuf>, Option<OutFileName>) {
     let odir = matches.opt_str("out-dir").map(|o| PathBuf::from(&o));
-    let ofile = matches.opt_str("o").map(|o| PathBuf::from(&o));
+    let ofile = matches.opt_str("o").map(|o| match o.as_str() {
+        "-" => OutFileName::Stdout,
+        path => OutFileName::Real(PathBuf::from(path)),
+    });
     (odir, ofile)
 }
 
@@ -685,7 +690,7 @@ fn print_crate_info(
                 for &style in &crate_types {
                     let fname =
                         rustc_session::output::filename_for_input(sess, style, id, &t_outputs);
-                    safe_println!("{}", fname.file_name().unwrap().to_string_lossy());
+                    safe_println!("{}", fname.as_path().file_name().unwrap().to_string_lossy());
                 }
             }
             Cfg => {
diff --git a/compiler/rustc_driver_impl/src/pretty.rs b/compiler/rustc_driver_impl/src/pretty.rs
index ee64b18d3f6..24a5f4030b8 100644
--- a/compiler/rustc_driver_impl/src/pretty.rs
+++ b/compiler/rustc_driver_impl/src/pretty.rs
@@ -9,7 +9,7 @@ use rustc_hir_pretty as pprust_hir;
 use rustc_middle::hir::map as hir_map;
 use rustc_middle::mir::{write_mir_graphviz, write_mir_pretty};
 use rustc_middle::ty::{self, TyCtxt};
-use rustc_session::config::{PpAstTreeMode, PpHirMode, PpMode, PpSourceMode};
+use rustc_session::config::{OutFileName, PpAstTreeMode, PpHirMode, PpMode, PpSourceMode};
 use rustc_session::Session;
 use rustc_span::symbol::Ident;
 use rustc_span::FileName;
@@ -359,8 +359,8 @@ fn get_source(sess: &Session) -> (String, FileName) {
 
 fn write_or_print(out: &str, sess: &Session) {
     match &sess.io.output_file {
-        None => print!("{out}"),
-        Some(p) => {
+        None | Some(OutFileName::Stdout) => print!("{out}"),
+        Some(OutFileName::Real(p)) => {
             if let Err(e) = std::fs::write(p, out) {
                 sess.emit_fatal(UnprettyDumpFail {
                     path: p.display().to_string(),
diff --git a/compiler/rustc_interface/Cargo.toml b/compiler/rustc_interface/Cargo.toml
index 2c7438ed9db..7826d42dcb2 100644
--- a/compiler/rustc_interface/Cargo.toml
+++ b/compiler/rustc_interface/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2021"
 [lib]
 
 [dependencies]
+atty = "0.2.13"
 libloading = "0.7.1"
 tracing = "0.1"
 rustc-rayon-core = { version = "0.5.0", optional = true }
diff --git a/compiler/rustc_interface/messages.ftl b/compiler/rustc_interface/messages.ftl
index be1a75f020b..bd9fad8b042 100644
--- a/compiler/rustc_interface/messages.ftl
+++ b/compiler/rustc_interface/messages.ftl
@@ -33,6 +33,7 @@ interface_mixed_proc_macro_crate =
 interface_multiple_output_types_adaption =
     due to multiple output types requested, the explicitly specified output file name will be adapted for each output type
 
+interface_multiple_output_types_to_stdout = can't use option `-o` or `--emit` to write multiple output types to stdout
 interface_out_dir_error =
     failed to find or create the directory specified by `--out-dir`
 
diff --git a/compiler/rustc_interface/src/errors.rs b/compiler/rustc_interface/src/errors.rs
index 0eedee25026..a9ab2720d89 100644
--- a/compiler/rustc_interface/src/errors.rs
+++ b/compiler/rustc_interface/src/errors.rs
@@ -108,3 +108,7 @@ pub struct IgnoringExtraFilename;
 #[derive(Diagnostic)]
 #[diag(interface_ignoring_out_dir)]
 pub struct IgnoringOutDir;
+
+#[derive(Diagnostic)]
+#[diag(interface_multiple_output_types_to_stdout)]
+pub struct MultipleOutputTypesToStdout;
diff --git a/compiler/rustc_interface/src/interface.rs b/compiler/rustc_interface/src/interface.rs
index 39d56897999..2edc72ba72e 100644
--- a/compiler/rustc_interface/src/interface.rs
+++ b/compiler/rustc_interface/src/interface.rs
@@ -14,7 +14,7 @@ use rustc_middle::{bug, ty};
 use rustc_parse::maybe_new_parser_from_source_str;
 use rustc_query_impl::QueryCtxt;
 use rustc_query_system::query::print_query_stack;
-use rustc_session::config::{self, ErrorOutputType, Input, OutputFilenames};
+use rustc_session::config::{self, ErrorOutputType, Input, OutFileName, OutputFilenames};
 use rustc_session::config::{CheckCfg, ExpectedValues};
 use rustc_session::lint;
 use rustc_session::parse::{CrateConfig, ParseSess};
@@ -252,7 +252,7 @@ pub struct Config {
 
     pub input: Input,
     pub output_dir: Option<PathBuf>,
-    pub output_file: Option<PathBuf>,
+    pub output_file: Option<OutFileName>,
     pub file_loader: Option<Box<dyn FileLoader + Send + Sync>>,
     pub locale_resources: &'static [&'static str],
 
diff --git a/compiler/rustc_interface/src/passes.rs b/compiler/rustc_interface/src/passes.rs
index 42d8d228091..83a74742f5b 100644
--- a/compiler/rustc_interface/src/passes.rs
+++ b/compiler/rustc_interface/src/passes.rs
@@ -24,7 +24,7 @@ use rustc_parse::{parse_crate_from_file, parse_crate_from_source_str, validate_a
 use rustc_passes::{self, hir_stats, layout_test};
 use rustc_plugin_impl as plugin;
 use rustc_resolve::Resolver;
-use rustc_session::config::{CrateType, Input, OutputFilenames, OutputType};
+use rustc_session::config::{CrateType, Input, OutFileName, OutputFilenames, OutputType};
 use rustc_session::cstore::{MetadataLoader, Untracked};
 use rustc_session::output::filename_for_input;
 use rustc_session::search_paths::PathKind;
@@ -373,19 +373,23 @@ fn generated_output_paths(
 ) -> Vec<PathBuf> {
     let mut out_filenames = Vec::new();
     for output_type in sess.opts.output_types.keys() {
-        let file = outputs.path(*output_type);
+        let out_filename = outputs.path(*output_type);
+        let file = out_filename.as_path().to_path_buf();
         match *output_type {
             // If the filename has been overridden using `-o`, it will not be modified
             // by appending `.rlib`, `.exe`, etc., so we can skip this transformation.
             OutputType::Exe if !exact_name => {
                 for crate_type in sess.crate_types().iter() {
                     let p = filename_for_input(sess, *crate_type, crate_name, outputs);
-                    out_filenames.push(p);
+                    out_filenames.push(p.as_path().to_path_buf());
                 }
             }
             OutputType::DepInfo if sess.opts.unstable_opts.dep_info_omit_d_target => {
                 // Don't add the dep-info output when omitting it from dep-info targets
             }
+            OutputType::DepInfo if out_filename.is_stdout() => {
+                // Don't add the dep-info output when it goes to stdout
+            }
             _ => {
                 out_filenames.push(file);
             }
@@ -452,7 +456,8 @@ fn write_out_deps(tcx: TyCtxt<'_>, outputs: &OutputFilenames, out_filenames: &[P
     if !sess.opts.output_types.contains_key(&OutputType::DepInfo) {
         return;
     }
-    let deps_filename = outputs.path(OutputType::DepInfo);
+    let deps_output = outputs.path(OutputType::DepInfo);
+    let deps_filename = deps_output.as_path();
 
     let result: io::Result<()> = try {
         // Build a list of files used to compile the output and
@@ -515,33 +520,47 @@ fn write_out_deps(tcx: TyCtxt<'_>, outputs: &OutputFilenames, out_filenames: &[P
             }
         }
 
-        let mut file = BufWriter::new(fs::File::create(&deps_filename)?);
-        for path in out_filenames {
-            writeln!(file, "{}: {}\n", path.display(), files.join(" "))?;
-        }
+        let write_deps_to_file = |file: &mut dyn Write| -> io::Result<()> {
+            for path in out_filenames {
+                writeln!(file, "{}: {}\n", path.display(), files.join(" "))?;
+            }
 
-        // Emit a fake target for each input file to the compilation. This
-        // prevents `make` from spitting out an error if a file is later
-        // deleted. For more info see #28735
-        for path in files {
-            writeln!(file, "{path}:")?;
-        }
+            // Emit a fake target for each input file to the compilation. This
+            // prevents `make` from spitting out an error if a file is later
+            // deleted. For more info see #28735
+            for path in files {
+                writeln!(file, "{path}:")?;
+            }
 
-        // Emit special comments with information about accessed environment variables.
-        let env_depinfo = sess.parse_sess.env_depinfo.borrow();
-        if !env_depinfo.is_empty() {
-            let mut envs: Vec<_> = env_depinfo
-                .iter()
-                .map(|(k, v)| (escape_dep_env(*k), v.map(escape_dep_env)))
-                .collect();
-            envs.sort_unstable();
-            writeln!(file)?;
-            for (k, v) in envs {
-                write!(file, "# env-dep:{k}")?;
-                if let Some(v) = v {
-                    write!(file, "={v}")?;
-                }
+            // Emit special comments with information about accessed environment variables.
+            let env_depinfo = sess.parse_sess.env_depinfo.borrow();
+            if !env_depinfo.is_empty() {
+                let mut envs: Vec<_> = env_depinfo
+                    .iter()
+                    .map(|(k, v)| (escape_dep_env(*k), v.map(escape_dep_env)))
+                    .collect();
+                envs.sort_unstable();
                 writeln!(file)?;
+                for (k, v) in envs {
+                    write!(file, "# env-dep:{k}")?;
+                    if let Some(v) = v {
+                        write!(file, "={v}")?;
+                    }
+                    writeln!(file)?;
+                }
+            }
+
+            Ok(())
+        };
+
+        match deps_output {
+            OutFileName::Stdout => {
+                let mut file = BufWriter::new(io::stdout());
+                write_deps_to_file(&mut file)?;
+            }
+            OutFileName::Real(ref path) => {
+                let mut file = BufWriter::new(fs::File::create(path)?);
+                write_deps_to_file(&mut file)?;
             }
         }
     };
diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs
index 28e719a40e5..77ee2b40e37 100644
--- a/compiler/rustc_interface/src/tests.rs
+++ b/compiler/rustc_interface/src/tests.rs
@@ -11,7 +11,7 @@ use rustc_session::config::InstrumentXRay;
 use rustc_session::config::TraitSolver;
 use rustc_session::config::{build_configuration, build_session_options, to_crate_config};
 use rustc_session::config::{
-    BranchProtection, Externs, OomStrategy, OutputType, OutputTypes, PAuthKey, PacRet,
+    BranchProtection, Externs, OomStrategy, OutFileName, OutputType, OutputTypes, PAuthKey, PacRet,
     ProcMacroExecutionStrategy, SymbolManglingVersion, WasiExecModel,
 };
 use rustc_session::config::{CFGuard, ExternEntry, LinkerPluginLto, LtoCli, SwitchWithOptPath};
@@ -167,8 +167,14 @@ fn test_output_types_tracking_hash_different_paths() {
     let mut v2 = Options::default();
     let mut v3 = Options::default();
 
-    v1.output_types = OutputTypes::new(&[(OutputType::Exe, Some(PathBuf::from("./some/thing")))]);
-    v2.output_types = OutputTypes::new(&[(OutputType::Exe, Some(PathBuf::from("/some/thing")))]);
+    v1.output_types = OutputTypes::new(&[(
+        OutputType::Exe,
+        Some(OutFileName::Real(PathBuf::from("./some/thing"))),
+    )]);
+    v2.output_types = OutputTypes::new(&[(
+        OutputType::Exe,
+        Some(OutFileName::Real(PathBuf::from("/some/thing"))),
+    )]);
     v3.output_types = OutputTypes::new(&[(OutputType::Exe, None)]);
 
     assert_non_crate_hash_different(&v1, &v2);
@@ -182,13 +188,13 @@ fn test_output_types_tracking_hash_different_construction_order() {
     let mut v2 = Options::default();
 
     v1.output_types = OutputTypes::new(&[
-        (OutputType::Exe, Some(PathBuf::from("./some/thing"))),
-        (OutputType::Bitcode, Some(PathBuf::from("./some/thing.bc"))),
+        (OutputType::Exe, Some(OutFileName::Real(PathBuf::from("./some/thing")))),
+        (OutputType::Bitcode, Some(OutFileName::Real(PathBuf::from("./some/thing.bc")))),
     ]);
 
     v2.output_types = OutputTypes::new(&[
-        (OutputType::Bitcode, Some(PathBuf::from("./some/thing.bc"))),
-        (OutputType::Exe, Some(PathBuf::from("./some/thing"))),
+        (OutputType::Bitcode, Some(OutFileName::Real(PathBuf::from("./some/thing.bc")))),
+        (OutputType::Exe, Some(OutFileName::Real(PathBuf::from("./some/thing")))),
     ]);
 
     assert_same_hash(&v1, &v2);
diff --git a/compiler/rustc_interface/src/util.rs b/compiler/rustc_interface/src/util.rs
index cb19750203e..87252fefb1e 100644
--- a/compiler/rustc_interface/src/util.rs
+++ b/compiler/rustc_interface/src/util.rs
@@ -11,7 +11,7 @@ use rustc_parse::validate_attr;
 use rustc_session as session;
 use rustc_session::config::CheckCfg;
 use rustc_session::config::{self, CrateType};
-use rustc_session::config::{ErrorOutputType, OutputFilenames};
+use rustc_session::config::{ErrorOutputType, OutFileName, OutputFilenames, OutputTypes};
 use rustc_session::filesearch::sysroot_candidates;
 use rustc_session::lint::{self, BuiltinLintDiagnostics, LintBuffer};
 use rustc_session::parse::CrateConfig;
@@ -500,7 +500,36 @@ pub fn collect_crate_types(session: &Session, attrs: &[ast::Attribute]) -> Vec<C
     base
 }
 
+fn multiple_output_types_to_stdout(
+    output_types: &OutputTypes,
+    single_output_file_is_stdout: bool,
+) -> bool {
+    if atty::is(atty::Stream::Stdout) {
+        // If stdout is a tty, check if multiple text output types are
+        // specified by `--emit foo=- --emit bar=-` or `-o - --emit foo,bar`
+        let named_text_types = output_types
+            .iter()
+            .filter(|(f, o)| f.is_text_output() && *o == &Some(OutFileName::Stdout))
+            .count();
+        let unnamed_text_types =
+            output_types.iter().filter(|(f, o)| f.is_text_output() && o.is_none()).count();
+        named_text_types > 1 || unnamed_text_types > 1 && single_output_file_is_stdout
+    } else {
+        // Otherwise, all the output types should be checked
+        let named_types =
+            output_types.values().filter(|o| *o == &Some(OutFileName::Stdout)).count();
+        let unnamed_types = output_types.values().filter(|o| o.is_none()).count();
+        named_types > 1 || unnamed_types > 1 && single_output_file_is_stdout
+    }
+}
+
 pub fn build_output_filenames(attrs: &[ast::Attribute], sess: &Session) -> OutputFilenames {
+    if multiple_output_types_to_stdout(
+        &sess.opts.output_types,
+        sess.io.output_file == Some(OutFileName::Stdout),
+    ) {
+        sess.emit_fatal(errors::MultipleOutputTypesToStdout);
+    }
     match sess.io.output_file {
         None => {
             // "-" as input file will cause the parser to read from stdin so we
@@ -544,7 +573,7 @@ pub fn build_output_filenames(attrs: &[ast::Attribute], sess: &Session) -> Outpu
 
             OutputFilenames::new(
                 out_file.parent().unwrap_or_else(|| Path::new("")).to_path_buf(),
-                out_file.file_stem().unwrap_or_default().to_str().unwrap().to_string(),
+                out_file.filestem().unwrap_or_default().to_str().unwrap().to_string(),
                 ofile,
                 sess.io.temps_dir.clone(),
                 sess.opts.cg.extra_filename.clone(),
diff --git a/compiler/rustc_metadata/messages.ftl b/compiler/rustc_metadata/messages.ftl
index 6d8601b9e2b..d6b08d840e3 100644
--- a/compiler/rustc_metadata/messages.ftl
+++ b/compiler/rustc_metadata/messages.ftl
@@ -4,6 +4,9 @@ metadata_as_needed_compatibility =
 metadata_bad_panic_strategy =
     the linked panic runtime `{$runtime}` is not compiled with this crate's panic strategy `{$strategy}`
 
+metadata_binary_output_to_tty =
+    option `-o` or `--emit` is used to write binary output type `metadata` to stdout, but stdout is a tty
+
 metadata_bundle_needs_static =
     linking modifier `bundle` is only compatible with `static` linking kind
 
@@ -63,6 +66,9 @@ metadata_fail_seek_file =
 metadata_fail_write_file =
     failed to write to the file: {$err}
 
+metadata_failed_copy_to_stdout =
+    failed to copy {$filename} to stdout: {$err}
+
 metadata_failed_create_encoded_metadata =
     failed to create encoded metadata from file: {$err}
 
@@ -72,6 +78,9 @@ metadata_failed_create_file =
 metadata_failed_create_tempdir =
     couldn't create a temp dir: {$err}
 
+metadata_failed_remove =
+    failed to remove {$filename}: {$err}
+
 metadata_failed_write_error =
     failed to write {$filename}: {$err}
 
diff --git a/compiler/rustc_metadata/src/errors.rs b/compiler/rustc_metadata/src/errors.rs
index a44c1dd582e..e110c68321d 100644
--- a/compiler/rustc_metadata/src/errors.rs
+++ b/compiler/rustc_metadata/src/errors.rs
@@ -395,6 +395,24 @@ pub struct FailedWriteError {
     pub err: Error,
 }
 
+#[derive(Diagnostic)]
+#[diag(metadata_failed_copy_to_stdout)]
+pub struct FailedCopyToStdout {
+    pub filename: PathBuf,
+    pub err: Error,
+}
+
+#[derive(Diagnostic)]
+#[diag(metadata_failed_remove)]
+pub struct FailedRemove {
+    pub filename: PathBuf,
+    pub err: Error,
+}
+
+#[derive(Diagnostic)]
+#[diag(metadata_binary_output_to_tty)]
+pub struct BinaryOutputToTty;
+
 #[derive(Diagnostic)]
 #[diag(metadata_missing_native_library)]
 pub struct MissingNativeLibrary<'a> {
diff --git a/compiler/rustc_metadata/src/fs.rs b/compiler/rustc_metadata/src/fs.rs
index 08de828fbdb..5be99c8e4c0 100644
--- a/compiler/rustc_metadata/src/fs.rs
+++ b/compiler/rustc_metadata/src/fs.rs
@@ -1,18 +1,19 @@
 use crate::errors::{
-    FailedCreateEncodedMetadata, FailedCreateFile, FailedCreateTempdir, FailedWriteError,
+    BinaryOutputToTty, FailedCopyToStdout, FailedCreateEncodedMetadata, FailedCreateFile,
+    FailedCreateTempdir, FailedRemove, FailedWriteError,
 };
 use crate::{encode_metadata, EncodedMetadata};
 
 use rustc_data_structures::temp_dir::MaybeTempDir;
 use rustc_hir::def_id::LOCAL_CRATE;
 use rustc_middle::ty::TyCtxt;
-use rustc_session::config::OutputType;
+use rustc_session::config::{OutFileName, OutputType};
 use rustc_session::output::filename_for_metadata;
 use rustc_session::{MetadataKind, Session};
 use tempfile::Builder as TempFileBuilder;
 
-use std::fs;
 use std::path::{Path, PathBuf};
+use std::{fs, io};
 
 // FIXME(eddyb) maybe include the crate name in this?
 pub const METADATA_FILENAME: &str = "lib.rmeta";
@@ -74,26 +75,47 @@ pub fn encode_and_write_metadata(tcx: TyCtxt<'_>) -> (EncodedMetadata, bool) {
     // this file always exists.
     let need_metadata_file = tcx.sess.opts.output_types.contains_key(&OutputType::Metadata);
     let (metadata_filename, metadata_tmpdir) = if need_metadata_file {
-        if let Err(err) = non_durable_rename(&metadata_filename, &out_filename) {
-            tcx.sess.emit_fatal(FailedWriteError { filename: out_filename, err });
-        }
+        let filename = match out_filename {
+            OutFileName::Real(ref path) => {
+                if let Err(err) = non_durable_rename(&metadata_filename, path) {
+                    tcx.sess.emit_fatal(FailedWriteError { filename: path.to_path_buf(), err });
+                }
+                path.clone()
+            }
+            OutFileName::Stdout => {
+                if out_filename.is_tty() {
+                    tcx.sess.emit_err(BinaryOutputToTty);
+                } else if let Err(err) = copy_to_stdout(&metadata_filename) {
+                    tcx.sess
+                        .emit_err(FailedCopyToStdout { filename: metadata_filename.clone(), err });
+                }
+                metadata_filename
+            }
+        };
         if tcx.sess.opts.json_artifact_notifications {
             tcx.sess
                 .parse_sess
                 .span_diagnostic
-                .emit_artifact_notification(&out_filename, "metadata");
+                .emit_artifact_notification(&out_filename.as_path(), "metadata");
         }
-        (out_filename, None)
+        (filename, None)
     } else {
         (metadata_filename, Some(metadata_tmpdir))
     };
 
     // Load metadata back to memory: codegen may need to include it in object files.
-    let metadata =
-        EncodedMetadata::from_path(metadata_filename, metadata_tmpdir).unwrap_or_else(|err| {
+    let metadata = EncodedMetadata::from_path(metadata_filename.clone(), metadata_tmpdir)
+        .unwrap_or_else(|err| {
             tcx.sess.emit_fatal(FailedCreateEncodedMetadata { err });
         });
 
+    // Delete the temporary metadata file if output is stdout
+    if need_metadata_file && out_filename.is_stdout() {
+        if let Err(err) = fs::remove_file(&metadata_filename) {
+            tcx.sess.emit_err(FailedRemove { filename: metadata_filename, err });
+        }
+    }
+
     let need_metadata_module = metadata_kind == MetadataKind::Compressed;
 
     (metadata, need_metadata_module)
@@ -116,3 +138,11 @@ pub fn non_durable_rename(src: &Path, dst: &Path) -> std::io::Result<()> {
     let _ = std::fs::remove_file(dst);
     std::fs::rename(src, dst)
 }
+
+pub fn copy_to_stdout(from: &Path) -> io::Result<()> {
+    let file = fs::File::open(from)?;
+    let mut reader = io::BufReader::new(file);
+    let mut stdout = io::stdout();
+    io::copy(&mut reader, &mut stdout)?;
+    Ok(())
+}
diff --git a/compiler/rustc_mir_transform/src/dump_mir.rs b/compiler/rustc_mir_transform/src/dump_mir.rs
index 746e3d9652d..13841be494c 100644
--- a/compiler/rustc_mir_transform/src/dump_mir.rs
+++ b/compiler/rustc_mir_transform/src/dump_mir.rs
@@ -7,7 +7,7 @@ use crate::MirPass;
 use rustc_middle::mir::write_mir_pretty;
 use rustc_middle::mir::Body;
 use rustc_middle::ty::TyCtxt;
-use rustc_session::config::OutputType;
+use rustc_session::config::{OutFileName, OutputType};
 
 pub struct Marker(pub &'static str);
 
@@ -20,8 +20,15 @@ impl<'tcx> MirPass<'tcx> for Marker {
 }
 
 pub fn emit_mir(tcx: TyCtxt<'_>) -> io::Result<()> {
-    let path = tcx.output_filenames(()).path(OutputType::Mir);
-    let mut f = io::BufWriter::new(File::create(&path)?);
-    write_mir_pretty(tcx, None, &mut f)?;
+    match tcx.output_filenames(()).path(OutputType::Mir) {
+        OutFileName::Stdout => {
+            let mut f = io::stdout();
+            write_mir_pretty(tcx, None, &mut f)?;
+        }
+        OutFileName::Real(path) => {
+            let mut f = io::BufWriter::new(File::create(&path)?);
+            write_mir_pretty(tcx, None, &mut f)?;
+        }
+    }
     Ok(())
 }
diff --git a/compiler/rustc_session/Cargo.toml b/compiler/rustc_session/Cargo.toml
index 3af83aaaaa8..90ad3f90f2c 100644
--- a/compiler/rustc_session/Cargo.toml
+++ b/compiler/rustc_session/Cargo.toml
@@ -4,6 +4,7 @@ version = "0.0.0"
 edition = "2021"
 
 [dependencies]
+atty = "0.2.13"
 getopts = "0.2"
 rustc_macros = { path = "../rustc_macros" }
 tracing = "0.1"
diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs
index 6c8c8e484f9..b72a95639e6 100644
--- a/compiler/rustc_session/src/config.rs
+++ b/compiler/rustc_session/src/config.rs
@@ -30,6 +30,7 @@ use std::collections::btree_map::{
     Iter as BTreeMapIter, Keys as BTreeMapKeysIter, Values as BTreeMapValuesIter,
 };
 use std::collections::{BTreeMap, BTreeSet};
+use std::ffi::OsStr;
 use std::fmt;
 use std::hash::Hash;
 use std::iter;
@@ -333,7 +334,7 @@ impl OutputType {
         }
     }
 
-    fn shorthand(&self) -> &'static str {
+    pub fn shorthand(&self) -> &'static str {
         match *self {
             OutputType::Bitcode => "llvm-bc",
             OutputType::Assembly => "asm",
@@ -386,6 +387,18 @@ impl OutputType {
             OutputType::Exe => "",
         }
     }
+
+    pub fn is_text_output(&self) -> bool {
+        match *self {
+            OutputType::Assembly
+            | OutputType::LlvmAssembly
+            | OutputType::Mir
+            | OutputType::DepInfo => true,
+            OutputType::Bitcode | OutputType::Object | OutputType::Metadata | OutputType::Exe => {
+                false
+            }
+        }
+    }
 }
 
 /// The type of diagnostics output to generate.
@@ -438,14 +451,14 @@ pub enum ResolveDocLinks {
 /// dependency tracking for command-line arguments. Also only hash keys, since tracking
 /// should only depend on the output types, not the paths they're written to.
 #[derive(Clone, Debug, Hash, HashStable_Generic)]
-pub struct OutputTypes(BTreeMap<OutputType, Option<PathBuf>>);
+pub struct OutputTypes(BTreeMap<OutputType, Option<OutFileName>>);
 
 impl OutputTypes {
-    pub fn new(entries: &[(OutputType, Option<PathBuf>)]) -> OutputTypes {
+    pub fn new(entries: &[(OutputType, Option<OutFileName>)]) -> OutputTypes {
         OutputTypes(BTreeMap::from_iter(entries.iter().map(|&(k, ref v)| (k, v.clone()))))
     }
 
-    pub fn get(&self, key: &OutputType) -> Option<&Option<PathBuf>> {
+    pub fn get(&self, key: &OutputType) -> Option<&Option<OutFileName>> {
         self.0.get(key)
     }
 
@@ -453,11 +466,15 @@ impl OutputTypes {
         self.0.contains_key(key)
     }
 
-    pub fn keys(&self) -> BTreeMapKeysIter<'_, OutputType, Option<PathBuf>> {
+    pub fn iter(&self) -> BTreeMapIter<'_, OutputType, Option<OutFileName>> {
+        self.0.iter()
+    }
+
+    pub fn keys(&self) -> BTreeMapKeysIter<'_, OutputType, Option<OutFileName>> {
         self.0.keys()
     }
 
-    pub fn values(&self) -> BTreeMapValuesIter<'_, OutputType, Option<PathBuf>> {
+    pub fn values(&self) -> BTreeMapValuesIter<'_, OutputType, Option<OutFileName>> {
         self.0.values()
     }
 
@@ -658,11 +675,71 @@ impl Input {
     }
 }
 
+#[derive(Clone, Hash, Debug, HashStable_Generic, PartialEq)]
+pub enum OutFileName {
+    Real(PathBuf),
+    Stdout,
+}
+
+impl OutFileName {
+    pub fn parent(&self) -> Option<&Path> {
+        match *self {
+            OutFileName::Real(ref path) => path.parent(),
+            OutFileName::Stdout => None,
+        }
+    }
+
+    pub fn filestem(&self) -> Option<&OsStr> {
+        match *self {
+            OutFileName::Real(ref path) => path.file_stem(),
+            OutFileName::Stdout => Some(OsStr::new("stdout")),
+        }
+    }
+
+    pub fn is_stdout(&self) -> bool {
+        match *self {
+            OutFileName::Real(_) => false,
+            OutFileName::Stdout => true,
+        }
+    }
+
+    pub fn is_tty(&self) -> bool {
+        match *self {
+            OutFileName::Real(_) => false,
+            OutFileName::Stdout => atty::is(atty::Stream::Stdout),
+        }
+    }
+
+    pub fn as_path(&self) -> &Path {
+        match *self {
+            OutFileName::Real(ref path) => path.as_ref(),
+            OutFileName::Stdout => &Path::new("stdout"),
+        }
+    }
+
+    /// For a given output filename, return the actual name of the file that
+    /// can be used to write codegen data of type `flavor`. For real-path
+    /// output filenames, this would be trivial as we can just use the path.
+    /// Otherwise for stdout, return a temporary path so that the codegen data
+    /// may be later copied to stdout.
+    pub fn file_for_writing(
+        &self,
+        outputs: &OutputFilenames,
+        flavor: OutputType,
+        codegen_unit_name: Option<&str>,
+    ) -> PathBuf {
+        match *self {
+            OutFileName::Real(ref path) => path.clone(),
+            OutFileName::Stdout => outputs.temp_path(flavor, codegen_unit_name),
+        }
+    }
+}
+
 #[derive(Clone, Hash, Debug, HashStable_Generic)]
 pub struct OutputFilenames {
     pub out_directory: PathBuf,
     filestem: String,
-    pub single_output_file: Option<PathBuf>,
+    pub single_output_file: Option<OutFileName>,
     pub temps_directory: Option<PathBuf>,
     pub outputs: OutputTypes,
 }
@@ -675,7 +752,7 @@ impl OutputFilenames {
     pub fn new(
         out_directory: PathBuf,
         out_filestem: String,
-        single_output_file: Option<PathBuf>,
+        single_output_file: Option<OutFileName>,
         temps_directory: Option<PathBuf>,
         extra: String,
         outputs: OutputTypes,
@@ -689,12 +766,12 @@ impl OutputFilenames {
         }
     }
 
-    pub fn path(&self, flavor: OutputType) -> PathBuf {
+    pub fn path(&self, flavor: OutputType) -> OutFileName {
         self.outputs
             .get(&flavor)
             .and_then(|p| p.to_owned())
             .or_else(|| self.single_output_file.clone())
-            .unwrap_or_else(|| self.output_path(flavor))
+            .unwrap_or_else(|| OutFileName::Real(self.output_path(flavor)))
     }
 
     /// Gets the output path where a compilation artifact of the given type
@@ -1821,7 +1898,10 @@ fn parse_output_types(
             for output_type in list.split(',') {
                 let (shorthand, path) = match output_type.split_once('=') {
                     None => (output_type, None),
-                    Some((shorthand, path)) => (shorthand, Some(PathBuf::from(path))),
+                    Some((shorthand, "-")) => (shorthand, Some(OutFileName::Stdout)),
+                    Some((shorthand, path)) => {
+                        (shorthand, Some(OutFileName::Real(PathBuf::from(path))))
+                    }
                 };
                 let output_type = OutputType::from_shorthand(shorthand).unwrap_or_else(|| {
                     early_error(
@@ -2892,7 +2972,7 @@ pub(crate) mod dep_tracking {
     use super::{
         BranchProtection, CFGuard, CFProtection, CrateType, DebugInfo, ErrorOutputType,
         InstrumentCoverage, InstrumentXRay, LdImpl, LinkerPluginLto, LocationDetail, LtoCli,
-        OomStrategy, OptLevel, OutputType, OutputTypes, Passes, ResolveDocLinks,
+        OomStrategy, OptLevel, OutFileName, OutputType, OutputTypes, Passes, ResolveDocLinks,
         SourceFileHashAlgorithm, SplitDwarfKind, SwitchWithOptPath, SymbolManglingVersion,
         TraitSolver, TrimmedDefPaths,
     };
@@ -2990,6 +3070,7 @@ pub(crate) mod dep_tracking {
         SourceFileHashAlgorithm,
         TrimmedDefPaths,
         Option<LdImpl>,
+        OutFileName,
         OutputType,
         RealFileName,
         LocationDetail,
diff --git a/compiler/rustc_session/src/output.rs b/compiler/rustc_session/src/output.rs
index fdb9fae44e1..2088744bc5b 100644
--- a/compiler/rustc_session/src/output.rs
+++ b/compiler/rustc_session/src/output.rs
@@ -1,5 +1,5 @@
 //! Related to out filenames of compilation (e.g. save analysis, binaries).
-use crate::config::{CrateType, Input, OutputFilenames, OutputType};
+use crate::config::{CrateType, Input, OutFileName, OutputFilenames, OutputType};
 use crate::errors::{
     CrateNameDoesNotMatch, CrateNameEmpty, CrateNameInvalid, FileIsNotWriteable,
     InvalidCharacterInCrateName,
@@ -8,14 +8,14 @@ use crate::Session;
 use rustc_ast::{self as ast, attr};
 use rustc_span::symbol::sym;
 use rustc_span::{Span, Symbol};
-use std::path::{Path, PathBuf};
+use std::path::Path;
 
 pub fn out_filename(
     sess: &Session,
     crate_type: CrateType,
     outputs: &OutputFilenames,
     crate_name: Symbol,
-) -> PathBuf {
+) -> OutFileName {
     let default_filename = filename_for_input(sess, crate_type, crate_name, outputs);
     let out_filename = outputs
         .outputs
@@ -24,7 +24,9 @@ pub fn out_filename(
         .or_else(|| outputs.single_output_file.clone())
         .unwrap_or(default_filename);
 
-    check_file_is_writeable(&out_filename, sess);
+    if let OutFileName::Real(ref path) = out_filename {
+        check_file_is_writeable(path, sess);
+    }
 
     out_filename
 }
@@ -112,7 +114,7 @@ pub fn filename_for_metadata(
     sess: &Session,
     crate_name: Symbol,
     outputs: &OutputFilenames,
-) -> PathBuf {
+) -> OutFileName {
     // If the command-line specified the path, use that directly.
     if let Some(Some(out_filename)) = sess.opts.output_types.get(&OutputType::Metadata) {
         return out_filename.clone();
@@ -120,12 +122,13 @@ pub fn filename_for_metadata(
 
     let libname = format!("{}{}", crate_name, sess.opts.cg.extra_filename);
 
-    let out_filename = outputs
-        .single_output_file
-        .clone()
-        .unwrap_or_else(|| outputs.out_directory.join(&format!("lib{libname}.rmeta")));
+    let out_filename = outputs.single_output_file.clone().unwrap_or_else(|| {
+        OutFileName::Real(outputs.out_directory.join(&format!("lib{libname}.rmeta")))
+    });
 
-    check_file_is_writeable(&out_filename, sess);
+    if let OutFileName::Real(ref path) = out_filename {
+        check_file_is_writeable(path, sess);
+    }
 
     out_filename
 }
@@ -135,23 +138,33 @@ pub fn filename_for_input(
     crate_type: CrateType,
     crate_name: Symbol,
     outputs: &OutputFilenames,
-) -> PathBuf {
+) -> OutFileName {
     let libname = format!("{}{}", crate_name, sess.opts.cg.extra_filename);
 
     match crate_type {
-        CrateType::Rlib => outputs.out_directory.join(&format!("lib{libname}.rlib")),
+        CrateType::Rlib => {
+            OutFileName::Real(outputs.out_directory.join(&format!("lib{libname}.rlib")))
+        }
         CrateType::Cdylib | CrateType::ProcMacro | CrateType::Dylib => {
             let (prefix, suffix) = (&sess.target.dll_prefix, &sess.target.dll_suffix);
-            outputs.out_directory.join(&format!("{prefix}{libname}{suffix}"))
+            OutFileName::Real(outputs.out_directory.join(&format!("{prefix}{libname}{suffix}")))
         }
         CrateType::Staticlib => {
             let (prefix, suffix) = (&sess.target.staticlib_prefix, &sess.target.staticlib_suffix);
-            outputs.out_directory.join(&format!("{prefix}{libname}{suffix}"))
+            OutFileName::Real(outputs.out_directory.join(&format!("{prefix}{libname}{suffix}")))
         }
         CrateType::Executable => {
             let suffix = &sess.target.exe_suffix;
             let out_filename = outputs.path(OutputType::Exe);
-            if suffix.is_empty() { out_filename } else { out_filename.with_extension(&suffix[1..]) }
+            if let OutFileName::Real(ref path) = out_filename {
+                if suffix.is_empty() {
+                    out_filename
+                } else {
+                    OutFileName::Real(path.with_extension(&suffix[1..]))
+                }
+            } else {
+                out_filename
+            }
         }
     }
 }
diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs
index bbe52dbced0..08e2f19e11b 100644
--- a/compiler/rustc_session/src/session.rs
+++ b/compiler/rustc_session/src/session.rs
@@ -2,7 +2,9 @@ use crate::cgu_reuse_tracker::CguReuseTracker;
 use crate::code_stats::CodeStats;
 pub use crate::code_stats::{DataTypeKind, FieldInfo, FieldKind, SizeKind, VariantInfo};
 use crate::config::Input;
-use crate::config::{self, CrateType, InstrumentCoverage, OptLevel, OutputType, SwitchWithOptPath};
+use crate::config::{
+    self, CrateType, InstrumentCoverage, OptLevel, OutFileName, OutputType, SwitchWithOptPath,
+};
 use crate::errors;
 use crate::parse::{add_feature_diagnostics, ParseSess};
 use crate::search_paths::{PathKind, SearchPath};
@@ -135,7 +137,7 @@ pub struct Limits {
 pub struct CompilerIO {
     pub input: Input,
     pub output_dir: Option<PathBuf>,
-    pub output_file: Option<PathBuf>,
+    pub output_file: Option<OutFileName>,
     pub temps_dir: Option<PathBuf>,
 }
 
diff --git a/src/doc/rustc/src/command-line-arguments.md b/src/doc/rustc/src/command-line-arguments.md
index 3be4382b0a3..2c15d1b1110 100644
--- a/src/doc/rustc/src/command-line-arguments.md
+++ b/src/doc/rustc/src/command-line-arguments.md
@@ -202,6 +202,12 @@ flag](codegen-options/index.md#extra-filename). The files are written to the
 current directory unless the [`--out-dir` flag](#option-out-dir) is used. Each
 emission type may also specify the output filename with the form `KIND=PATH`,
 which takes precedence over the `-o` flag.
+Specifying `-o -` or `--emit KIND=-` asks rustc to emit to stdout.
+Text output types (`asm`, `dep-info`, `llvm-ir` and `mir`) can be written to
+stdout despite it being a tty or not. This will result in an error if any
+binary output type is written to stdout that is a tty.
+This will also result in an error if multiple output types
+would be written to stdout, because they would be all mixed together.
 
 [LLVM bitcode]: https://llvm.org/docs/BitCodeFormat.html
 [LLVM IR]: https://llvm.org/docs/LangRef.html
diff --git a/tests/run-make-fulldeps/hotplug_codegen_backend/the_backend.rs b/tests/run-make-fulldeps/hotplug_codegen_backend/the_backend.rs
index 7db100a08a1..b971037ea67 100644
--- a/tests/run-make-fulldeps/hotplug_codegen_backend/the_backend.rs
+++ b/tests/run-make-fulldeps/hotplug_codegen_backend/the_backend.rs
@@ -62,7 +62,7 @@ impl CodegenBackend for TheBackend {
         codegen_results: CodegenResults,
         outputs: &OutputFilenames,
     ) -> Result<(), ErrorGuaranteed> {
-        use rustc_session::{config::CrateType, output::out_filename};
+        use rustc_session::{config::{CrateType, OutFileName}, output::out_filename};
         use std::io::Write;
         let crate_name = codegen_results.crate_info.local_crate_name;
         for &crate_type in sess.opts.crate_types.iter() {
@@ -70,8 +70,16 @@ impl CodegenBackend for TheBackend {
                 sess.fatal(format!("Crate type is {:?}", crate_type));
             }
             let output_name = out_filename(sess, crate_type, &outputs, crate_name);
-            let mut out_file = ::std::fs::File::create(output_name).unwrap();
-            write!(out_file, "This has been \"compiled\" successfully.").unwrap();
+            match output_name {
+                OutFileName::Real(ref path) => {
+                    let mut out_file = ::std::fs::File::create(path).unwrap();
+                    write!(out_file, "This has been \"compiled\" successfully.").unwrap();
+                }
+                OutFileName::Stdout => {
+                    let mut stdout = std::io::stdout();
+                    write!(stdout, "This has been \"compiled\" successfully.").unwrap();
+                }
+            }
         }
         Ok(())
     }
diff --git a/tests/run-make-fulldeps/issue-19371/foo.rs b/tests/run-make-fulldeps/issue-19371/foo.rs
index 53ec79e477b..6d08cfd07f8 100644
--- a/tests/run-make-fulldeps/issue-19371/foo.rs
+++ b/tests/run-make-fulldeps/issue-19371/foo.rs
@@ -6,7 +6,7 @@ extern crate rustc_session;
 extern crate rustc_span;
 
 use rustc_interface::interface;
-use rustc_session::config::{Input, Options, OutputType, OutputTypes};
+use rustc_session::config::{Input, Options, OutFileName, OutputType, OutputTypes};
 use rustc_span::source_map::FileName;
 
 use std::path::PathBuf;
@@ -50,7 +50,7 @@ fn compile(code: String, output: PathBuf, sysroot: PathBuf) {
         crate_cfg: Default::default(),
         crate_check_cfg: Default::default(),
         input,
-        output_file: Some(output),
+        output_file: Some(OutFileName::Real(output)),
         output_dir: None,
         file_loader: None,
         locale_resources: &[],
diff --git a/tests/run-make/emit-to-stdout/Makefile b/tests/run-make/emit-to-stdout/Makefile
new file mode 100644
index 00000000000..b7455965cff
--- /dev/null
+++ b/tests/run-make/emit-to-stdout/Makefile
@@ -0,0 +1,51 @@
+include ../tools.mk
+
+SRC=test.rs
+OUT=$(TMPDIR)/out
+
+all: asm llvm-ir dep-info mir llvm-bc obj metadata link multiple-types multiple-types-option-o
+
+asm: $(OUT)
+	$(RUSTC) --emit asm=$(OUT)/$@ $(SRC)
+	$(RUSTC) --emit asm=- $(SRC) | diff - $(OUT)/$@
+llvm-ir: $(OUT)
+	$(RUSTC) --emit llvm-ir=$(OUT)/$@ $(SRC)
+	$(RUSTC) --emit llvm-ir=- $(SRC) | diff - $(OUT)/$@
+dep-info: $(OUT)
+	$(RUSTC) -Z dep-info-omit-d-target=yes --emit dep-info=$(OUT)/$@ $(SRC)
+	$(RUSTC) --emit dep-info=- $(SRC) | diff - $(OUT)/$@
+mir: $(OUT)
+	$(RUSTC) --emit mir=$(OUT)/$@ $(SRC)
+	$(RUSTC) --emit mir=- $(SRC) | diff - $(OUT)/$@
+
+llvm-bc: $(OUT)
+	$(RUSTC) --emit llvm-bc=- $(SRC) 1>/dev/ptmx 2>$(OUT)/$@ || true
+	diff $(OUT)/$@ emit-llvm-bc.stderr
+obj: $(OUT)
+	$(RUSTC) --emit obj=- $(SRC) 1>/dev/ptmx 2>$(OUT)/$@ || true
+	diff $(OUT)/$@ emit-obj.stderr
+
+# For metadata output, a temporary directory will be created to hold the temporary
+# metadata file. But when output is stdout, the temporary directory will be located
+# in the same place as $(SRC), which is mounted as read-only in the tests. Thus as
+# a workaround, $(SRC) is copied to the test output directory $(OUT) and we compile
+# it there.
+metadata: $(OUT)
+	cp $(SRC) $(OUT)
+	(cd $(OUT); pwd; ls -d; $(RUSTC) --emit metadata=- $(SRC) 1>/dev/ptmx 2>$(OUT)/$@ || true)
+	diff $(OUT)/$@ emit-metadata.stderr
+
+link: $(OUT)
+	$(RUSTC) --emit link=- $(SRC) 1>/dev/ptmx 2>$(OUT)/$@ || true
+	diff $(OUT)/$@ emit-link.stderr
+
+multiple-types: $(OUT)
+	$(RUSTC) --emit asm=- --emit llvm-ir=- --emit dep-info=- --emit mir=- $(SRC) 2>$(OUT)/$@ || true
+	diff $(OUT)/$@ emit-multiple-types.stderr
+
+multiple-types-option-o: $(OUT)
+	$(RUSTC) -o - --emit asm,llvm-ir,dep-info,mir $(SRC) 2>$(OUT)/$@ || true
+	diff $(OUT)/$@ emit-multiple-types.stderr
+
+$(OUT):
+	mkdir -p $(OUT)
diff --git a/tests/run-make/emit-to-stdout/emit-link.stderr b/tests/run-make/emit-to-stdout/emit-link.stderr
new file mode 100644
index 00000000000..a9d856503e6
--- /dev/null
+++ b/tests/run-make/emit-to-stdout/emit-link.stderr
@@ -0,0 +1,4 @@
+error: option `-o` or `--emit` is used to write binary output type `link` to stdout, but stdout is a tty
+
+error: aborting due to previous error
+
diff --git a/tests/run-make/emit-to-stdout/emit-llvm-bc.stderr b/tests/run-make/emit-to-stdout/emit-llvm-bc.stderr
new file mode 100644
index 00000000000..7b53bd16e7a
--- /dev/null
+++ b/tests/run-make/emit-to-stdout/emit-llvm-bc.stderr
@@ -0,0 +1,4 @@
+error: option `-o` or `--emit` is used to write binary output type `llvm-bc` to stdout, but stdout is a tty
+
+error: aborting due to previous error
+
diff --git a/tests/run-make/emit-to-stdout/emit-metadata.stderr b/tests/run-make/emit-to-stdout/emit-metadata.stderr
new file mode 100644
index 00000000000..ee1e52937b9
--- /dev/null
+++ b/tests/run-make/emit-to-stdout/emit-metadata.stderr
@@ -0,0 +1,4 @@
+error: option `-o` or `--emit` is used to write binary output type `metadata` to stdout, but stdout is a tty
+
+error: aborting due to previous error
+
diff --git a/tests/run-make/emit-to-stdout/emit-multiple-types.stderr b/tests/run-make/emit-to-stdout/emit-multiple-types.stderr
new file mode 100644
index 00000000000..b8a683cd9eb
--- /dev/null
+++ b/tests/run-make/emit-to-stdout/emit-multiple-types.stderr
@@ -0,0 +1,4 @@
+error: can't use option `-o` or `--emit` to write multiple output types to stdout
+
+error: aborting due to previous error
+
diff --git a/tests/run-make/emit-to-stdout/emit-obj.stderr b/tests/run-make/emit-to-stdout/emit-obj.stderr
new file mode 100644
index 00000000000..b1303530844
--- /dev/null
+++ b/tests/run-make/emit-to-stdout/emit-obj.stderr
@@ -0,0 +1,4 @@
+error: option `-o` or `--emit` is used to write binary output type `obj` to stdout, but stdout is a tty
+
+error: aborting due to previous error
+
diff --git a/tests/run-make/emit-to-stdout/test.rs b/tests/run-make/emit-to-stdout/test.rs
new file mode 100644
index 00000000000..c1bfaa6cab5
--- /dev/null
+++ b/tests/run-make/emit-to-stdout/test.rs
@@ -0,0 +1 @@
+#![crate_type = "rlib"]