Drupal commerce how to make shipments(shipping) programmatically

Drupal commerce shipping module is complex, there are few moving parts that you need to consider when making custom changes to it. First there are shipment types which are associated with order type, this is usually done through UI, unless you also add order type programmatically. So lets say you associated some default shipment type with some default order type all through UI and still want to associate custom shipping methods, which can and are options you can choose (by default this is chosen by customer in checkout). I will assume you have and order/cart object and we will start from there. We can check if there are some shipments already associated with this order

      if ($order->get('shipments')->count() == 0) {

if there are none, we should make first one

 $first_shipment = Shipment::create([
          'type' => 'default',
          'order_id' => $order_id,
          'title' => 'Shipment',
          'state' => 'ready',
        ]);

We created default shipment with order id and state that is ready (check others in commerce shipping module).
But this is just bare bones part, we want to add items that are being shipped, for that, you need to loop through order items and add associated data for them. We add weight, we add, quantity and value for items:

  // Loop through order items and add them to shipment
        foreach ($order->getItems() as $order_item) {
          $quantity = $order_item->getQuantity();
          $purchased_entity = $order_item->getPurchasedEntity();

          if ($purchased_entity->get('weight')->isEmpty()) {
            $weight = new Weight(1, WeightUnit::GRAM);
          }
          else {
            $weight_item = $purchased_entity->get('weight')->first();
            $weight = $weight_item->toMeasurement();
          }

          $shipment_item = new ShipmentItem([
            'order_item_id' => $order_item->id(),
            'title' => $purchased_entity->label(),
            'quantity' => $quantity,
            'weight' => $weight->multiply($quantity),
            'declared_value' => $order_item->getTotalPrice(),
          ]);
          $first_shipment->addItem($shipment_item);
        }

This is not all yes, we also need to set shipping method for this order, shipping methods can be configured in UI with conditions, we can also make some code that makes a proper selection but in this case, we are just going to use first one in the list.

       // Find rate, we use the first one, pretending there is only one we want to use, fixed rate for order
        $shipping_method_storage = \Drupal::entityTypeManager()->getStorage('commerce_shipping_method');
        $shipping_methods = $shipping_method_storage->loadMultipleForShipment($first_shipment);
        $first_shipment->setShippingMethod(reset($shipping_methods));

after that we also want to set an amount for our shipment, this will be charged in order, this can be taken as total calculation for particular shipping, it can be flat or it can be dynamic, in this case we use flat rate and set it:

        $shipping_method_plugin = reset($shipping_methods)->getPlugin();
        $shipping_rates = $shipping_method_plugin->calculateRates($first_shipment);
        $shipping_service = $shipping_rate->getService()->id();
        $amount = reset($shipping_rates)->getAmount();
        $first_shipment->setAmount($amount);

after all that we need to save out shipment object and then also assign it as first shipment to our order:

        $first_shipment->save();
        $order->set('shipments', [$first_shipment]);
        $order->save();

and final part is that shipments need to have shipping profile, so we then need to create a profile associated with this shipment:

      $profile = \Drupal::entityTypeManager()->getStorage('profile')->create(['uid' => $order->getCustomerId(), 'type' =>'customer',]);
      $profile->address->given_name = $given_name;
      $profile->address->family_name = $family_name;

      $profile->address->given_name = $given_name;
      $profile->address->family_name = $family_name;
      $profile->address->country_code = $shipping_info['country'];
      $profile->address->locality = $shipping_info['city'];
      $profile->address->administrative_area = $shipping_info['dependentLocality'];
      $profile->address->postal_code = $shipping_info['postalCode'];
      $profile->address->address_line1 = $shipping_info["addressLine"][0];
      $profile->address->address_line2 = isset($shipping_info["addressLine"][1])?:"";
      $profile->save();

      $first_shipment->setShippingProfile($profile);
      $first_shipment->setShippingService($shipping_service);
      $first_shipment->save();
      $order->set('shipments', $first_shipment);
      $order->save();

When all this is done, you will have custom programmatically shipment done and associated with your order.